Skip to main content

atecc608b/command/
sign.rs

1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! `Sign` command.
17//!
18//! Produces an ECDSA P-256 signature using a private key stored in a slot.
19//!
20//! The driver exposes the "External" sign mode only: the host first loads
21//! a 32-byte message digest into the chip's `TempKey` register via a
22//! passthrough [`AteccChannel::nonce_passthrough`] call, then issues
23//! [`AteccChannel::sign_external`]. The "Internal" sign mode (where the chip
24//! signs a digest it computed itself in a previous operation) is not used
25//! in this project's workflow and is therefore not exposed.
26//!
27//! Reference: `CryptoAuthLib` `lib/calib/calib_sign.c`, constants
28//! `SIGN_MODE_EXTERNAL` (0x80), `SIGN_MODE_INTERNAL` (0x00).
29//!
30//! # Signature format
31//!
32//! The returned 64 bytes are the raw `R || S` form (each 32 bytes,
33//! big-endian). To convert to the ASN.1 DER form used by many TLS or
34//! certificate libraries, the higher layer must do so explicitly: the
35//! driver returns the chip output verbatim.
36
37use crate::driver::AteccChannel;
38use crate::error::AteccError;
39use crate::hal::AteccHal;
40use crate::opcodes::{EXEC_TIME_SIGN_MS, OP_SIGN};
41use crate::slot::Slot;
42
43/// Size of the returned ECDSA P-256 signature (`R || S`).
44pub const SIGNATURE_SIZE: usize = 64;
45
46/// `param1` mode bits for `Sign(external)` on the ATECC608.
47///
48/// Combines two sub-flags:
49/// - bit 7 (`0x80`) — `SIGN_MODE_EXTERNAL`: the message-to-sign is a
50///   32-byte digest supplied by the host, not an internal chip state.
51/// - bit 5 (`0x20`) — `SIGN_MODE_SOURCE_MSGDIGBUF`: take the digest
52///   from the Message Digest Buffer (the 608's dedicated 32-byte
53///   register), as opposed to `TempKey`.
54///
55/// **The 608's `Sign(external)` strictly requires the `MsgDigBuf` source.**
56/// Sending `0x80` alone (no source bit) on a 608 makes the chip include
57/// extra context bytes — serial number, OTP, etc. — in what it actually
58/// signs, so the resulting signature does NOT verify against the raw
59/// digest off-chip. Reference: `lib/calib/calib_sign.c::calib_sign` in
60/// `CryptoAuthLib`, which selects this mode for `ATECC608`. The legacy
61/// `0x80`-only encoding still works on the older ATECC108A / ATECC508A.
62///
63/// Callers must load the digest via
64/// [`AteccChannel::nonce_passthrough`] with target
65/// [`crate::command::nonce::NonceTarget::MsgDigBuf`] immediately before
66/// this command.
67const SIGN_MODE_EXTERNAL_FROM_MSGDIGBUF: u8 = 0xA0;
68
69impl<H> AteccChannel<'_, H>
70where
71    H: AteccHal,
72{
73    /// Sign the 32-byte digest currently loaded in the Message Digest
74    /// Buffer with the private key in `slot`.
75    ///
76    /// Callers must load the digest via [`AteccChannel::nonce_passthrough`]
77    /// with target [`crate::command::nonce::NonceTarget::MsgDigBuf`]
78    /// immediately before this call. Any intervening command that
79    /// overwrites `MsgDigBuf` invalidates the operation.
80    ///
81    /// Returns the 64-byte raw signature `R || S`, both big-endian.
82    /// `R` then `S`, each 32 bytes; this is the on-the-wire layout the
83    /// chip returns and matches the convention used by every standard
84    /// ECDSA verifier when the signature components are passed
85    /// separately.
86    ///
87    /// # Errors
88    /// See [`AteccChannel::execute_command`]. The chip rejects this
89    /// command if the target slot has `ReqAuth=1` and no authenticated
90    /// session is active, or if `MsgDigBuf` was not properly seeded by a
91    /// preceding `Nonce(passthrough, target=MsgDigBuf)`.
92    pub async fn sign_external
93    (
94        &mut self,
95        slot: Slot,
96    ) -> Result<[u8; SIGNATURE_SIZE], AteccError<H::Error>>
97    {
98        // Response: count(1) + 64 signature + crc(2) = 67 bytes.
99        let mut response_buf = [0u8; 1 + SIGNATURE_SIZE + 2];
100        let param2 = u16::from(slot.as_u8());
101        let payload = self
102            .execute_command
103            (
104                OP_SIGN,
105                SIGN_MODE_EXTERNAL_FROM_MSGDIGBUF,
106                param2,
107                &[],
108                EXEC_TIME_SIGN_MS,
109                &mut response_buf,
110            )
111            .await?;
112
113        let bytes: &[u8; SIGNATURE_SIZE] = payload
114            .try_into()
115            .map_err(|_| AteccError::MalformedResponse)?;
116        Ok(*bytes)
117    }
118}
119
120#[cfg(test)]
121mod tests
122{
123    use super::*;
124
125    #[test]
126    fn external_mode_matches_cryptoauthlib_constant()
127    {
128        // CryptoAuthLib `calib_sign` for ATECC608 selects
129        // `SIGN_MODE_EXTERNAL | SIGN_MODE_SOURCE_MSGDIGBUF` = `0x80 | 0x20`.
130        assert_eq!(SIGN_MODE_EXTERNAL_FROM_MSGDIGBUF, 0xA0);
131    }
132
133    #[test]
134    fn signature_size_is_64()
135    {
136        assert_eq!(SIGNATURE_SIZE, 64);
137    }
138}