iota_sdk_crypto/
passkey.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use iota_types::{PasskeyAuthenticator, SimpleSignature, UserSignature};
6use signature::Verifier;
7
8use crate::{SignatureError, secp256r1::Secp256r1VerifyingKey};
9
10#[derive(Default, Clone, Debug)]
11pub struct PasskeyVerifier {}
12
13impl PasskeyVerifier {
14    pub fn new() -> Self {
15        Self {}
16    }
17}
18
19impl Verifier<PasskeyAuthenticator> for PasskeyVerifier {
20    fn verify(
21        &self,
22        message: &[u8],
23        authenticator: &PasskeyAuthenticator,
24    ) -> Result<(), SignatureError> {
25        let SimpleSignature::Secp256r1 {
26            signature,
27            public_key,
28        } = authenticator.signature()
29        else {
30            return Err(SignatureError::from_source("not a secp256r1 signature"));
31        };
32
33        if message != authenticator.challenge() {
34            return Err(SignatureError::from_source(
35                "passkey challenge does not match expected message",
36            ));
37        }
38
39        // Construct passkey signing message = authenticator_data ||
40        // sha256(client_data_json).
41        let mut message = authenticator.authenticator_data().to_owned();
42        let client_data_hash = {
43            use sha2::Digest;
44
45            let mut hasher = sha2::Sha256::new();
46            hasher.update(authenticator.client_data_json().as_bytes());
47            hasher.finalize()
48        };
49        message.extend_from_slice(&client_data_hash);
50
51        let verifying_key = Secp256r1VerifyingKey::new(&public_key)?;
52
53        verifying_key.verify(&message, &signature)
54    }
55}
56
57impl Verifier<UserSignature> for PasskeyVerifier {
58    fn verify(&self, message: &[u8], signature: &UserSignature) -> Result<(), SignatureError> {
59        let UserSignature::Passkey(authenticator) = signature else {
60            return Err(SignatureError::from_source("not a passkey authenticator"));
61        };
62
63        <Self as Verifier<PasskeyAuthenticator>>::verify(self, message, authenticator)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use iota_types::Transaction;
70    #[cfg(target_arch = "wasm32")]
71    use wasm_bindgen_test::wasm_bindgen_test as test;
72
73    use super::*;
74    use crate::IotaVerifier;
75
76    #[test]
77    fn transaction_signing_fixture() {
78        let transaction = "AAAAACdZawPnpJRjmVcwDu6xrIumtq5NLO+6GHbs0iGdCoD7AQ0T0TolicYERdSvyCRjSSduDZLbSpBsZBoib+lF48EBcgAAAAAAAAAgpQr/Mudl9BdzyBdkbqTlqBw4/aJ21kAD/jpJKa05im4nWWsD56SUY5lXMA7usayLprauTSzvuhh27NIhnQqA++gDAAAAAAAAgIQeAAAAAAAA";
79        let signature = "BiVJlg3liA6MaHQ0Fw9kdmBbj+SuuaKGMseZXPO6gx2XYx0AAAAAhgF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCJjaGFsbGVuZ2UiOiJXellBZmVvbHcweU15bEFheDRvbzNjVC1rdEVaM0xmenZXcURqakxKZVRvIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo1MTczIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfWICfOgpQ38QYao9Gj0/bqmWYNkuxvbuN3lz4uzFcXeVMEVivX41eC9H+tk+UnvUvKzThtf+uMLFzerU0zZLi8le4QJJsAUcyjsP/1UPAesax8UOC14M62FjAqtqaR46wR7jCg==";
80
81        let transaction: Transaction = {
82            use base64ct::Encoding;
83            let bytes = base64ct::Base64::decode_vec(transaction).unwrap();
84            bcs::from_bytes(&bytes).unwrap()
85        };
86        let signature = UserSignature::from_base64(signature).unwrap();
87
88        let verifier = PasskeyVerifier::default();
89        verifier
90            .verify_transaction(&transaction, &signature)
91            .unwrap();
92    }
93}