nut20.rs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. //! Mint Quote Signatures
  2. use std::str::FromStr;
  3. use bitcoin::secp256k1::schnorr::Signature;
  4. use thiserror::Error;
  5. use super::{MintRequest, PublicKey, SecretKey};
  6. /// Nut19 Error
  7. #[derive(Debug, Error)]
  8. pub enum Error {
  9. /// Signature not provided
  10. #[error("Signature not provided")]
  11. SignatureMissing,
  12. /// Quote signature invalid signature
  13. #[error("Quote signature invalid signature")]
  14. InvalidSignature,
  15. /// Nut01 error
  16. #[error(transparent)]
  17. NUT01(#[from] crate::nuts::nut01::Error),
  18. }
  19. impl<Q> MintRequest<Q>
  20. where
  21. Q: ToString,
  22. {
  23. /// Constructs the message to be signed according to NUT-20 specification.
  24. ///
  25. /// The message is constructed by concatenating (as UTF-8 encoded bytes):
  26. /// 1. The quote ID (as UTF-8)
  27. /// 2. All blinded secrets (B_0 through B_n) converted to hex strings (as UTF-8)
  28. ///
  29. /// Format: `quote_id || B_0 || B_1 || ... || B_n`
  30. /// where each component is encoded as UTF-8 bytes
  31. pub fn msg_to_sign(&self) -> Vec<u8> {
  32. // Pre-calculate capacity to avoid reallocations
  33. let quote_id = self.quote.to_string();
  34. let capacity = quote_id.len() + (self.outputs.len() * 66);
  35. let mut msg = Vec::with_capacity(capacity);
  36. msg.append(&mut quote_id.clone().into_bytes()); // String.into_bytes() produces UTF-8
  37. for output in &self.outputs {
  38. // to_hex() creates a hex string, into_bytes() converts it to UTF-8 bytes
  39. msg.append(&mut output.blinded_secret.to_hex().into_bytes());
  40. }
  41. msg
  42. }
  43. /// Sign [`MintRequest`]
  44. pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> {
  45. let msg = self.msg_to_sign();
  46. let signature: Signature = secret_key.sign(&msg)?;
  47. self.signature = Some(signature.to_string());
  48. Ok(())
  49. }
  50. /// Verify signature on [`MintRequest`]
  51. pub fn verify_signature(&self, pubkey: PublicKey) -> Result<(), Error> {
  52. let signature = self.signature.as_ref().ok_or(Error::SignatureMissing)?;
  53. let signature = Signature::from_str(signature).map_err(|_| Error::InvalidSignature)?;
  54. let msg_to_sign = self.msg_to_sign();
  55. pubkey.verify(&msg_to_sign, &signature)?;
  56. Ok(())
  57. }
  58. }
  59. #[cfg(test)]
  60. mod tests {
  61. use uuid::Uuid;
  62. use super::*;
  63. #[test]
  64. fn test_msg_to_sign() {
  65. let request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
  66. // let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79";
  67. let expected_msg_to_sign = [
  68. 57, 100, 55, 52, 53, 50, 55, 48, 45, 49, 52, 48, 53, 45, 52, 54, 100, 101, 45, 98, 53,
  69. 99, 53, 45, 101, 50, 55, 54, 50, 98, 52, 102, 53, 101, 48, 48, 48, 51, 52, 50, 101, 53,
  70. 98, 99, 99, 55, 55, 102, 53, 98, 50, 97, 51, 99, 50, 97, 102, 98, 52, 48, 98, 98, 53,
  71. 57, 49, 97, 49, 101, 50, 55, 100, 97, 56, 51, 99, 100, 100, 99, 57, 54, 56, 97, 98,
  72. 100, 99, 48, 101, 99, 52, 57, 48, 52, 50, 48, 49, 97, 50, 48, 49, 56, 51, 52, 48, 51,
  73. 50, 102, 100, 51, 99, 52, 100, 99, 52, 57, 97, 50, 56, 52, 52, 97, 56, 57, 57, 57, 56,
  74. 100, 53, 101, 57, 100, 53, 98, 48, 102, 48, 98, 48, 48, 100, 100, 101, 57, 51, 49, 48,
  75. 48, 54, 51, 97, 99, 98, 56, 97, 57, 50, 101, 50, 102, 100, 97, 102, 97, 52, 49, 50, 54,
  76. 100, 52, 48, 51, 51, 98, 54, 102, 100, 101, 53, 48, 98, 54, 97, 48, 100, 102, 101, 54,
  77. 49, 97, 100, 49, 52, 56, 102, 102, 102, 49, 54, 55, 97, 100, 57, 99, 102, 56, 51, 48,
  78. 56, 100, 101, 100, 53, 102, 54, 102, 54, 98, 50, 102, 101, 48, 48, 48, 97, 48, 51, 54,
  79. 99, 52, 54, 52, 99, 51, 49, 49, 48, 50, 98, 101, 53, 97, 53, 53, 102, 48, 51, 101, 53,
  80. 99, 48, 97, 97, 101, 97, 55, 55, 53, 57, 53, 100, 53, 55, 52, 98, 99, 101, 57, 50, 99,
  81. 54, 100, 53, 55, 97, 50, 97, 48, 102, 98, 50, 98, 53, 57, 53, 53, 99, 48, 98, 56, 55,
  82. 101, 52, 53, 50, 48, 101, 48, 54, 98, 53, 51, 48, 50, 50, 48, 57, 102, 99, 50, 56, 55,
  83. 51, 102, 50, 56, 53, 50, 49, 99, 98, 100, 100, 101, 55, 102, 55, 98, 51, 98, 98, 49,
  84. 53, 50, 49, 48, 48, 50, 52, 54, 51, 102, 53, 57, 55, 57, 54, 56, 54, 102, 100, 49, 53,
  85. 54, 102, 50, 51, 102, 101, 54, 97, 56, 97, 97, 50, 98, 55, 57,
  86. ]
  87. .to_vec();
  88. let request_msg_to_sign = request.msg_to_sign();
  89. assert_eq!(expected_msg_to_sign, request_msg_to_sign);
  90. }
  91. #[test]
  92. fn test_valid_signature() {
  93. let pubkey = PublicKey::from_hex(
  94. "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
  95. )
  96. .unwrap();
  97. let request: MintRequest<Uuid> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap();
  98. assert!(request.verify_signature(pubkey).is_ok());
  99. }
  100. #[test]
  101. fn test_mint_request_signature() {
  102. let mut request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap();
  103. let secret =
  104. SecretKey::from_hex("50d7fd7aa2b2fe4607f41f4ce6f8794fc184dd47b8cdfbe4b3d1249aa02d35aa")
  105. .unwrap();
  106. request.sign(secret.clone()).unwrap();
  107. assert!(request.verify_signature(secret.public_key()).is_ok());
  108. }
  109. #[test]
  110. fn test_invalid_signature() {
  111. let pubkey = PublicKey::from_hex(
  112. "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
  113. )
  114. .unwrap();
  115. let request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
  116. // Signature is on a different quote id verification should fail
  117. assert!(request.verify_signature(pubkey).is_err());
  118. }
  119. }