From 01010c763f3b59bf6166647851491d1e948fcd21 Mon Sep 17 00:00:00 2001 From: Wilson Silva Date: Thu, 14 Mar 2024 22:03:26 +0000 Subject: [PATCH] Allow the verification of signatures and events Added the methods: - Event#verify_signature - Crypto#check_sig! - Crypto#valid_sig? - Crypto#sign_message Fixed a primitive obsession by introducing a Signature class to ensure that signatures are valid Nostr signatures. --- adr/0002-introduction-of-signature-class.md | 27 +++ lib/nostr.rb | 1 + lib/nostr/crypto.rb | 88 ++++++++- lib/nostr/errors.rb | 4 + .../errors/invalid_signature_format_error.rb | 18 ++ .../errors/invalid_signature_length_error.rb | 18 ++ .../errors/invalid_signature_type_error.rb | 16 ++ .../errors/signature_validation_error.rb | 6 + lib/nostr/event.rb | 28 +++ lib/nostr/signature.rb | 67 +++++++ sig/nostr/crypto.rbs | 3 + .../errors/invalid_signature_format_error.rbs | 5 + .../errors/invalid_signature_length_error.rbs | 5 + .../errors/invalid_signature_type_error.rbs | 5 + .../errors/signature_validation_error.rbs | 4 + sig/nostr/event.rbs | 5 +- sig/nostr/signature.rbs | 14 ++ sig/vendor/schnorr.rbs | 4 +- sig/vendor/schnorr/signature.rbs | 16 ++ spec/nostr/crypto_spec.rb | 69 +++++++ .../invalid_signature_format_error_spec.rb | 13 ++ .../invalid_signature_length_error_spec.rb | 13 ++ .../invalid_signature_type_error_spec.rb | 13 ++ spec/nostr/event_spec.rb | 186 ++++++++++++++---- spec/nostr/signature_spec.rb | 56 ++++++ 25 files changed, 637 insertions(+), 47 deletions(-) create mode 100644 adr/0002-introduction-of-signature-class.md create mode 100644 lib/nostr/errors/invalid_signature_format_error.rb create mode 100644 lib/nostr/errors/invalid_signature_length_error.rb create mode 100644 lib/nostr/errors/invalid_signature_type_error.rb create mode 100644 lib/nostr/errors/signature_validation_error.rb create mode 100644 lib/nostr/signature.rb create mode 100644 sig/nostr/errors/invalid_signature_format_error.rbs create mode 100644 sig/nostr/errors/invalid_signature_length_error.rbs create mode 100644 sig/nostr/errors/invalid_signature_type_error.rbs create mode 100644 sig/nostr/errors/signature_validation_error.rbs create mode 100644 sig/nostr/signature.rbs create mode 100644 sig/vendor/schnorr/signature.rbs create mode 100644 spec/nostr/errors/invalid_signature_format_error_spec.rb create mode 100644 spec/nostr/errors/invalid_signature_length_error_spec.rb create mode 100644 spec/nostr/errors/invalid_signature_type_error_spec.rb create mode 100644 spec/nostr/signature_spec.rb diff --git a/adr/0002-introduction-of-signature-class.md b/adr/0002-introduction-of-signature-class.md new file mode 100644 index 0000000..ac809b3 --- /dev/null +++ b/adr/0002-introduction-of-signature-class.md @@ -0,0 +1,27 @@ +# 2. introduction-of-signature-class + +Date: 2024-03-14 + +## Status + +Accepted + +## Context + +I noticed significant overuse of primitive strings for signatures, which led to widespread and repetitive validation logic, increasing the potential for errors and making the system harder to manage and maintain. + +## Decision + +I introduced the Nostr::Signature class, choosing to subclass String to leverage string-like behavior while embedding specific validation rules for signatures. This move was aimed at streamlining validation, ensuring consistency, and maintaining the usability of strings. + +## Consequences + +### Positive + +- This design choice has made the codebase cleaner and more robust, reducing the chances of errors related to signature handling. It ensures that all signature instances are valid at creation, leveraging the familiarity and flexibility of string operations without sacrificing the integrity of the data. Moreover, it sets a strong foundation for extending signature-related functionality in the future. + +### Negative + +- __Performance Concerns:__ Subclassing String might introduce slight performance overheads due to the additional validation logic executed upon instantiation of a Signature object. +- __Integration Challenges:__ Integrating this class into existing systems where strings were used indiscriminately for signatures requires careful refactoring to ensure compatibility. There's also the potential for issues when passing Nostr::Signature objects to libraries or APIs expecting plain strings without the additional constraints. +- __Learning Curve:__ For new team members or contributors, understanding the necessity and functionality of the Nostr::Signature class adds to the learning curve, potentially slowing down initial development efforts as they familiarize themselves with the custom implementation. diff --git a/lib/nostr.rb b/lib/nostr.rb index f89a7a7..2e2f712 100644 --- a/lib/nostr.rb +++ b/lib/nostr.rb @@ -12,6 +12,7 @@ require_relative 'nostr/relay' require_relative 'nostr/relay_message_type' require_relative 'nostr/key_pair' require_relative 'nostr/event_kind' +require_relative 'nostr/signature' require_relative 'nostr/event' require_relative 'nostr/events/encrypted_direct_message' require_relative 'nostr/client' diff --git a/lib/nostr/crypto.rb b/lib/nostr/crypto.rb index afd20ca..8554537 100644 --- a/lib/nostr/crypto.rb +++ b/lib/nostr/crypto.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Nostr - # Performs cryptographic operations on a +Nostr::Event+. + # Performs cryptographic operations. class Crypto # Numeric base of the OpenSSL big number used in an event content's encryption. # @@ -90,17 +90,93 @@ module Nostr # def sign_event(event, private_key) event_digest = hash_event(event) - - hex_private_key = Array(private_key).pack('H*') - hex_message = Array(event_digest).pack('H*') - event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*') + signature = sign_message(event_digest, private_key) event.id = event_digest - event.sig = event_signature + event.sig = signature event end + # Signs a message using the Schnorr signature algorithm + # + # @api public + # + # @example Signing a message + # crypto = Nostr::Crypto.new + # message = 'Viva la libertad carajo' + # private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d') + # signature = crypto.sign_message(message, private_key) + # signature # => 'b2115694a576f5bdcebf8c0951a3c7adcfbdb17b11cb9e6d6b7017691138bc6' \ + # '38fee642a7bd26f71b313a7057181294198900a9770d1435e43f182acf3d34c26' + # + # @param [String] message The message to be signed + # @param [PrivateKey] private_key The private key used for signing + # + # @return [Signature] A signature object containing the signature as a 64-byte hexadecimal string. + # + def sign_message(message, private_key) + hex_private_key = Array(private_key).pack('H*') + hex_message = Array(message).pack('H*') + hex_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*') + + Signature.new(hex_signature.to_s) + end + + # Verifies the given {Signature} and returns true if it is valid + # + # @api public + # + # @example Checking a signature + # public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6') + # private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d') + # message = 'Viva la libertad carajo' + # crypto = Nostr::Crypto.new + # signature = crypto.sign_message(message, private_key) + # valid = crypto.valid_sig?(message, public_key, signature) + # valid # => true + # + # @see #check_sig! + # + # @param [String] message A message to be signed with binary format. + # @param [PublicKey] public_key The public key with binary format. + # @param [Signature] signature The signature with binary format. + # + # @return [Boolean] whether signature is valid. + # + def valid_sig?(message, public_key, signature) + signature = Schnorr::Signature.decode([signature].pack('H*')) + Schnorr.valid_sig?([message].pack('H*'), [public_key].pack('H*'), signature.encode) + end + + # Verifies the given {Signature} and raises an +Schnorr::InvalidSignatureError+ if it is invalid + # + # @api public + # + # @example Checking a signature + # public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6') + # private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d') + # message = 'Viva la libertad carajo' + # crypto = Nostr::Crypto.new + # signature = crypto.sign_message(message, private_key) + # valid = crypto.valid_sig?(message, public_key, signature) + # valid # => true + # + # @see #valid_sig? + # + # @param [String] message A message to be signed with binary format. + # @param [PublicKey] public_key The public key with binary format. + # @param [Signature] signature The signature with binary format. + # + # @raise [Schnorr::InvalidSignatureError] if the signature is invalid. + # + # @return [Boolean] whether signature is valid. + # + def check_sig!(message, public_key, signature) + signature = Schnorr::Signature.decode([signature].pack('H*')) + Schnorr.check_sig!([message].pack('H*'), [public_key].pack('H*'), signature.encode) + end + private # Finds a shared key between two keys diff --git a/lib/nostr/errors.rb b/lib/nostr/errors.rb index 32f48ad..f83fca5 100644 --- a/lib/nostr/errors.rb +++ b/lib/nostr/errors.rb @@ -6,3 +6,7 @@ require_relative 'errors/invalid_hrp_error' require_relative 'errors/invalid_key_type_error' require_relative 'errors/invalid_key_length_error' require_relative 'errors/invalid_key_format_error' +require_relative 'errors/signature_validation_error' +require_relative 'errors/invalid_signature_type_error' +require_relative 'errors/invalid_signature_length_error' +require_relative 'errors/invalid_signature_format_error' diff --git a/lib/nostr/errors/invalid_signature_format_error.rb b/lib/nostr/errors/invalid_signature_format_error.rb new file mode 100644 index 0000000..ac48619 --- /dev/null +++ b/lib/nostr/errors/invalid_signature_format_error.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Nostr + # Raised when the signature is in an invalid format + # + # @api public + # + class InvalidSignatureFormatError < SignatureValidationError + # Initializes the error + # + # @example + # InvalidSignatureFormatError.new + # + def initialize + super('Only lowercase hexadecimal characters are allowed in signatures.') + end + end +end diff --git a/lib/nostr/errors/invalid_signature_length_error.rb b/lib/nostr/errors/invalid_signature_length_error.rb new file mode 100644 index 0000000..ea5178d --- /dev/null +++ b/lib/nostr/errors/invalid_signature_length_error.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Nostr + # Raised when the signature's length is not 128 characters + # + # @api public + # + class InvalidSignatureLengthError < SignatureValidationError + # Initializes the error + # + # @example + # InvalidSignatureLengthError.new + # + def initialize + super('Invalid signature length. It should have 128 characters.') + end + end +end diff --git a/lib/nostr/errors/invalid_signature_type_error.rb b/lib/nostr/errors/invalid_signature_type_error.rb new file mode 100644 index 0000000..bc389c9 --- /dev/null +++ b/lib/nostr/errors/invalid_signature_type_error.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Nostr + # Raised when the signature is not a string + # + # @api public + # + class InvalidSignatureTypeError < SignatureValidationError + # Initializes the error + # + # @example + # InvalidSignatureTypeError.new + # + def initialize = super('Invalid signature type') + end +end diff --git a/lib/nostr/errors/signature_validation_error.rb b/lib/nostr/errors/signature_validation_error.rb new file mode 100644 index 0000000..85738b6 --- /dev/null +++ b/lib/nostr/errors/signature_validation_error.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Nostr + # Base class for all signature validation errors + class SignatureValidationError < Error; end +end diff --git a/lib/nostr/event.rb b/lib/nostr/event.rb index 6448b52..6f54677 100644 --- a/lib/nostr/event.rb +++ b/lib/nostr/event.rb @@ -181,6 +181,34 @@ module Nostr crypto.sign_event(self, private_key) end + # Verifies if the signature of the event is valid. A valid signature means that the event was signed by the owner + # + # @api public + # + # @example Verifying the signature of an event + # event = Nostr::Event.new( + # id: '90b75b78daf883ae57fbcc414d43faa028560b3211ee58e4ea82bf395bb82042', + # pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + # created_at: 1667422587, + # kind: Nostr::EventKind::TEXT_NOTE, + # content: 'Your feedback is appreciated, now pay $8', + # sig: '32f18adebe942e19b171c1c7d2fb27ce794dfea4155e289dca7952b43ed1ec39' \ + # '1d3dc198ba2761bc6d40c737a6eaf4edcc8963acabd3bfcebd04f16637025bdc' + # ) + # + # event.verify_signature # => true + # + # @return [Boolean] Whether the signature is valid or not. + # + def verify_signature + crypto = Crypto.new + + return false if id.nil? || pubkey.nil? + return false if sig.nil? # FIXME: See https://github.com/soutaro/steep/issues/1079 + + crypto.valid_sig?(id, pubkey, sig) + end + # Serializes the event, to obtain a SHA256 digest of it # # @api public diff --git a/lib/nostr/signature.rb b/lib/nostr/signature.rb new file mode 100644 index 0000000..528012e --- /dev/null +++ b/lib/nostr/signature.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Nostr + # 64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data, + # which is the same as the "id" field + class Signature < String + # The regular expression for hexadecimal lowercase characters + # + # @return [Regexp] The regular expression for hexadecimal lowercase characters + # + FORMAT = /^[a-f0-9]+$/ + + # The length of the signature in hex format + # + # @return [Integer] The length of the signature in hex format + # + LENGTH = 128 + + # Instantiates a new Signature + # + # @api public + # + # @example Instantiating a new signature + # Nostr::Signature.new( + # 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + # '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' + # ) + # + # @param [String] hex_value Hex-encoded value of the signature + # + # @raise [SignatureValidationError] + # + def initialize(hex_value) + validate(hex_value) + + super(hex_value) + end + + private + + # Hex-encoded value of the signature + # + # @api private + # + # @return [String] hex_value Hex-encoded value of the signature + # + attr_reader :hex_value + + # Validates the hex value of the signature + # + # @api private + # + # @param [String] hex_value The signature in hex format + # + # @raise InvalidSignatureTypeError when the signature is not a string + # @raise InvalidSignatureLengthError when the signature's length is not 128 characters + # @raise InvalidSignatureFormatError when the signature is in an invalid format + # + # @return [void] + # + def validate(hex_value) + raise InvalidSignatureTypeError unless hex_value.is_a?(String) + raise InvalidSignatureLengthError unless hex_value.size == LENGTH + raise InvalidSignatureFormatError unless hex_value.match(FORMAT) + end + end +end diff --git a/sig/nostr/crypto.rbs b/sig/nostr/crypto.rbs index f582040..c3bb3f4 100644 --- a/sig/nostr/crypto.rbs +++ b/sig/nostr/crypto.rbs @@ -7,6 +7,9 @@ module Nostr def encrypt_text: (PrivateKey, PublicKey, String) -> String def decrypt_text: (PrivateKey, PublicKey, String) -> String def sign_event: (Event, PrivateKey) -> Event + def sign_message: (String, PrivateKey) -> Signature + def valid_sig?: (String, PublicKey, Signature) -> bool + def check_sig!: (String, PublicKey, Signature) -> bool private diff --git a/sig/nostr/errors/invalid_signature_format_error.rbs b/sig/nostr/errors/invalid_signature_format_error.rbs new file mode 100644 index 0000000..54906ae --- /dev/null +++ b/sig/nostr/errors/invalid_signature_format_error.rbs @@ -0,0 +1,5 @@ +module Nostr + class InvalidSignatureFormatError < SignatureValidationError + def initialize: -> void + end +end diff --git a/sig/nostr/errors/invalid_signature_length_error.rbs b/sig/nostr/errors/invalid_signature_length_error.rbs new file mode 100644 index 0000000..ce9f3a7 --- /dev/null +++ b/sig/nostr/errors/invalid_signature_length_error.rbs @@ -0,0 +1,5 @@ +module Nostr + class InvalidSignatureLengthError < SignatureValidationError + def initialize: -> void + end +end diff --git a/sig/nostr/errors/invalid_signature_type_error.rbs b/sig/nostr/errors/invalid_signature_type_error.rbs new file mode 100644 index 0000000..4456af2 --- /dev/null +++ b/sig/nostr/errors/invalid_signature_type_error.rbs @@ -0,0 +1,5 @@ +module Nostr + class InvalidSignatureTypeError < SignatureValidationError + def initialize: -> void + end +end diff --git a/sig/nostr/errors/signature_validation_error.rbs b/sig/nostr/errors/signature_validation_error.rbs new file mode 100644 index 0000000..748239d --- /dev/null +++ b/sig/nostr/errors/signature_validation_error.rbs @@ -0,0 +1,4 @@ +module Nostr + class SignatureValidationError < Error + end +end diff --git a/sig/nostr/event.rbs b/sig/nostr/event.rbs index fe4798b..a2a8a89 100644 --- a/sig/nostr/event.rbs +++ b/sig/nostr/event.rbs @@ -6,7 +6,7 @@ module Nostr attr_reader tags: Array[Array[String]] attr_reader content: String attr_accessor id: String?|nil - attr_accessor sig: String?|nil + attr_accessor sig: Signature? def initialize: ( pubkey: PublicKey, @@ -15,7 +15,7 @@ module Nostr ?created_at: Integer, ?tags: Array[Array[String]], ?id: String|nil, - ?sig: String|nil + ?sig: Signature? ) -> void def serialize: -> [Integer, String, Integer, Integer, Array[Array[String]], String] @@ -32,6 +32,7 @@ module Nostr def ==: (Event other) -> bool def sign:(PrivateKey) -> Event + def verify_signature: -> bool def add_event_reference: (String) -> Array[Array[String]] def add_pubkey_reference: (PublicKey) -> Array[Array[String]] diff --git a/sig/nostr/signature.rbs b/sig/nostr/signature.rbs new file mode 100644 index 0000000..8c6ee5f --- /dev/null +++ b/sig/nostr/signature.rbs @@ -0,0 +1,14 @@ +module Nostr + class Signature < String + FORMAT: Regexp + LENGTH: int + + def initialize: (String) -> void + + private + + attr_reader hex_value: String + + def validate: (String) -> nil + end +end diff --git a/sig/vendor/schnorr.rbs b/sig/vendor/schnorr.rbs index fc542ce..1d7f4ab 100644 --- a/sig/vendor/schnorr.rbs +++ b/sig/vendor/schnorr.rbs @@ -1,4 +1,6 @@ # Added only to satisfy the Steep requirements. Not 100% reliable. module Schnorr - def self.sign: (String message, String private_key, ?String aux_rand) -> untyped + def self.sign: (String message, String private_key, ?String aux_rand) -> Signature + def self.valid_sig?: (String message, String public_key, String signature) -> bool + def self.check_sig!: (String message, String public_key, String signature) -> bool end diff --git a/sig/vendor/schnorr/signature.rbs b/sig/vendor/schnorr/signature.rbs new file mode 100644 index 0000000..c4e9150 --- /dev/null +++ b/sig/vendor/schnorr/signature.rbs @@ -0,0 +1,16 @@ +# Added only to satisfy the Steep requirements. Not 100% reliable. +module Schnorr + class InvalidSignatureError < StandardError + end + + class Signature + attr_reader r: Integer + attr_reader s: Integer + + def self.decode: (String string) -> Signature + + def initialize: (Integer r, Integer s) -> void + def encode: -> String + def ==: (untyped other) -> bool + end +end diff --git a/spec/nostr/crypto_spec.rb b/spec/nostr/crypto_spec.rb index c51887e..c679f8d 100644 --- a/spec/nostr/crypto_spec.rb +++ b/spec/nostr/crypto_spec.rb @@ -5,6 +5,62 @@ require 'spec_helper' RSpec.describe Nostr::Crypto do let(:crypto) { described_class.new } + describe '#check_sig!' do + let(:keypair) do + Nostr::KeyPair.new( + public_key: Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6'), + private_key: Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d') + ) + end + let(:message) { 'Your feedback is appreciated, now pay $8' } + + context 'when the signature is valid' do + it 'returns true' do + signature = crypto.sign_message(message, keypair.private_key) + + expect(crypto.check_sig!(message, keypair.public_key, signature)).to be(true) + end + end + + context 'when the signature is invalid' do + it 'raises an error' do + signature = Nostr::Signature.new('badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb' \ + 'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb') + + expect do + crypto.check_sig!(message, keypair.public_key, signature) + end.to raise_error(Schnorr::InvalidSignatureError, 'signature verification failed.') + end + end + end + + describe '#valid_sig?' do + let(:keypair) do + Nostr::KeyPair.new( + public_key: Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6'), + private_key: Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d') + ) + end + let(:message) { 'Your feedback is appreciated, now pay $8' } + + context 'when the signature is valid' do + it 'returns true' do + signature = crypto.sign_message(message, keypair.private_key) + + expect(crypto.valid_sig?(message, keypair.public_key, signature)).to be(true) + end + end + + context 'when the signature is invalid' do + it 'returns false' do + signature = Nostr::Signature.new('badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb' \ + 'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb') + + expect(crypto.valid_sig?(message, keypair.public_key, signature)).to be(false) + end + end + end + describe '#sign_event' do let(:keypair) do Nostr::KeyPair.new( @@ -31,6 +87,19 @@ RSpec.describe Nostr::Crypto do end end + describe '#sign_message' do + let(:private_key) { Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d') } + let(:message) { 'Your feedback is appreciated, now pay $8' } + + it 'signs a message' do + signature = crypto.sign_message(message, private_key) + hex_signature = '0fa6d8e26f44ddad9eca5be2b8a25d09338c1767f8bfce384046c8eb771d1120e4bda5ca49' \ + '27e74837f912d4810945af6abf8d38139c1347f2d71ba8c52b175b' + + expect(signature).to eq(Nostr::Signature.new(hex_signature)) + end + end + describe '#encrypt_text' do let(:sender_keypair) do Nostr::KeyPair.new( diff --git a/spec/nostr/errors/invalid_signature_format_error_spec.rb b/spec/nostr/errors/invalid_signature_format_error_spec.rb new file mode 100644 index 0000000..5af7918 --- /dev/null +++ b/spec/nostr/errors/invalid_signature_format_error_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::InvalidSignatureFormatError do + describe '#initialize' do + let(:error) { described_class.new } + + it 'builds a useful error message' do + expect(error.message).to eq('Only lowercase hexadecimal characters are allowed in signatures.') + end + end +end diff --git a/spec/nostr/errors/invalid_signature_length_error_spec.rb b/spec/nostr/errors/invalid_signature_length_error_spec.rb new file mode 100644 index 0000000..7a8054b --- /dev/null +++ b/spec/nostr/errors/invalid_signature_length_error_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::InvalidSignatureLengthError do + describe '#initialize' do + let(:error) { described_class.new } + + it 'builds a useful error message' do + expect(error.message).to eq('Invalid signature length. It should have 128 characters.') + end + end +end diff --git a/spec/nostr/errors/invalid_signature_type_error_spec.rb b/spec/nostr/errors/invalid_signature_type_error_spec.rb new file mode 100644 index 0000000..d38db3d --- /dev/null +++ b/spec/nostr/errors/invalid_signature_type_error_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::InvalidSignatureTypeError do + describe '#initialize' do + let(:error) { described_class.new } + + it 'builds a useful error message' do + expect(error.message).to eq('Invalid signature type') + end + end +end diff --git a/spec/nostr/event_spec.rb b/spec/nostr/event_spec.rb index c0aabce..81f8482 100644 --- a/spec/nostr/event_spec.rb +++ b/spec/nostr/event_spec.rb @@ -5,17 +5,17 @@ require 'spec_helper' RSpec.describe Nostr::Event do let(:event) do described_class.new( - id: '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9', - pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460', - created_at: 1_230_981_305, + id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, kind: 1, tags: [ %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e] ], content: 'Your feedback is appreciated, now pay $8', - sig: '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \ - '63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe' + sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' ) end @@ -24,8 +24,8 @@ RSpec.describe Nostr::Event do it 'returns true' do event1 = described_class.new( id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811', - pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558', - created_at: 1_230_981_305, + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, kind: 1, tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]], content: 'Your feedback is appreciated, now pay $8', @@ -35,8 +35,8 @@ RSpec.describe Nostr::Event do event2 = described_class.new( id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811', - pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558', - created_at: 1_230_981_305, + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, kind: 1, tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]], content: 'Your feedback is appreciated, now pay $8', @@ -52,8 +52,8 @@ RSpec.describe Nostr::Event do it 'returns false' do event1 = described_class.new( id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811', - pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558', - created_at: 1_230_981_305, + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, kind: 1, tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]], content: 'Your feedback is appreciated, now pay $8', @@ -63,13 +63,13 @@ RSpec.describe Nostr::Event do event2 = described_class.new( id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811', - pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558', - created_at: 1_230_981_305, + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, kind: 1, tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]], content: 'Your feedback is appreciated, now pay $8', - sig: '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \ - '63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe' + sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' ) expect(event1).not_to eq(event2) @@ -80,17 +80,17 @@ RSpec.describe Nostr::Event do describe '.new' do it 'creates an instance of an event' do event = described_class.new( - id: '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9', - pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460', - created_at: 1_230_981_305, + id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, kind: 1, tags: [ %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e] ], content: 'Your feedback is appreciated, now pay $8', - sig: '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \ - '63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe' + sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' ) expect(event).to be_an_instance_of(described_class) @@ -100,7 +100,7 @@ RSpec.describe Nostr::Event do describe '#add_event_reference' do let(:event) do described_class.new( - pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), kind: Nostr::EventKind::TEXT_NOTE, tags: [ %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e] @@ -124,7 +124,7 @@ RSpec.describe Nostr::Event do describe '#add_pubkey_reference' do let(:event) do described_class.new( - pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), kind: Nostr::EventKind::TEXT_NOTE, tags: [ %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408] @@ -153,7 +153,7 @@ RSpec.describe Nostr::Event do describe '#created_at' do it 'exposes the event creation date' do - expect(event.created_at).to eq(1_230_981_305) + expect(event.created_at).to eq(1_667_422_587) end end @@ -165,13 +165,13 @@ RSpec.describe Nostr::Event do describe '#id' do it 'exposes the event id' do - expect(event.id).to eq('20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9') + expect(event.id).to eq('499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba') end end describe '#id=' do it 'sets the event id' do - new_id = '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9' + new_id = '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba' event.id = new_id expect(event.id).to eq(new_id) @@ -180,7 +180,7 @@ RSpec.describe Nostr::Event do describe '#pubkey' do it 'exposes the event pubkey' do - expect(event.pubkey).to eq('ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460') + expect(event.pubkey).to eq('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558') end end @@ -191,8 +191,8 @@ RSpec.describe Nostr::Event do expect(serialized_event).to eq( [ 0, - 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460', - 1_230_981_305, + Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + 1_667_422_587, 1, [ %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], @@ -207,16 +207,16 @@ RSpec.describe Nostr::Event do describe '#sig' do it 'exposes the event signature' do expect(event.sig).to eq( - '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \ - '63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe' + 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' ) end end describe '#sig=' do it 'sets the event signature' do - new_signature = '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \ - '63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe' + new_signature = 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' event.sig = new_signature expect(event.sig).to eq(new_signature) @@ -226,7 +226,7 @@ RSpec.describe Nostr::Event do describe '#sign' do let(:event) do described_class.new( - pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), kind: Nostr::EventKind::TEXT_NOTE, content: 'Your feedback is appreciated, now pay $8' ) @@ -262,16 +262,126 @@ RSpec.describe Nostr::Event do describe '#to_h' do it 'converts the event to a hash' do expect(event.to_h).to eq( - id: '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9', - pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460', - created_at: 1_230_981_305, + id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, kind: 1, tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]], content: 'Your feedback is appreciated, now pay $8', - sig: '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \ - '63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe' + sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' ) end end + + describe '#verify_signature' do + context 'when the id is nil' do + let(:event) do + described_class.new( + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, + kind: 1, + tags: [ + %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], + %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e] + ], + content: 'Your feedback is appreciated, now pay $8', + sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' + ) + end + + it 'returns false' do + expect(event.verify_signature).to be(false) + end + end + + context 'when the sig is nil' do + let(:event) do + described_class.new( + id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, + kind: 1, + tags: [ + %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], + %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e] + ], + content: 'Your feedback is appreciated, now pay $8' + ) + end + + it 'returns false' do + event.sig = nil + expect(event.verify_signature).to be(false) + end + end + + context 'when the pubkey is missing' do + let(:event) do + described_class.new( + id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba', + pubkey: nil, + created_at: 1_667_422_587, + kind: 1, + tags: [ + %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], + %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e] + ], + content: 'Your feedback is appreciated, now pay $8', + sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' + ) + end + + it 'returns false' do + expect(event.verify_signature).to be(false) + end + end + + context 'when the sig is valid' do + let(:event) do + described_class.new( + id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, + kind: 1, + tags: [ + %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], + %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e] + ], + content: 'Your feedback is appreciated, now pay $8', + sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' + ) + end + + it 'returns true' do + expect(event.verify_signature).to be(true) + end + end + + context 'when the sig is invalid' do + let(:event) do + described_class.new( + id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba', + pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), + created_at: 1_667_422_587, + kind: 1, + tags: [ + %w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408], + %w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e] + ], + content: 'Your feedback is appreciated, now pay $8', + sig: 'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb' \ + 'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb' + ) + end + + it 'returns false' do + expect(event.verify_signature).to be(false) + end + end + end end diff --git a/spec/nostr/signature_spec.rb b/spec/nostr/signature_spec.rb new file mode 100644 index 0000000..da20b36 --- /dev/null +++ b/spec/nostr/signature_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::Signature do + let(:valid_hex_signature) do + 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \ + '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128' + end + + let(:signature) { described_class.new(valid_hex_signature) } + + describe '.new' do + context 'when the signature is not a string' do + it 'raises an InvalidSignatureTypeError' do + expect { described_class.new(1234) }.to raise_error( + Nostr::InvalidSignatureTypeError, + 'Invalid signature type' + ) + end + end + + context "when the signature's length is not 128 characters" do + it 'raises an InvalidSignatureLengthError' do + expect { described_class.new('a' * 129) }.to raise_error( + Nostr::InvalidSignatureLengthError, + 'Invalid signature length. It should have 128 characters.' + ) + end + end + + context 'when the signature contains non-hexadecimal characters' do + it 'raises an InvalidKeyFormatError' do + expect { described_class.new('g' * 128) }.to raise_error( + Nostr::InvalidSignatureFormatError, + 'Only lowercase hexadecimal characters are allowed in signatures.' + ) + end + end + + context 'when the signature contains uppercase characters' do + it 'raises an InvalidKeyFormatError' do + expect { described_class.new('A' * 128) }.to raise_error( + Nostr::InvalidSignatureFormatError, + 'Only lowercase hexadecimal characters are allowed in signatures.' + ) + end + end + + context 'when the signature is valid' do + it 'does not raise any error' do + expect { described_class.new('a' * 128) }.not_to raise_error + end + end + end +end