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.
This commit is contained in:
Wilson Silva 2024-03-14 22:03:26 +00:00
parent f8893f9b0e
commit 01010c763f
No known key found for this signature in database
GPG Key ID: 65135F94E23F82C8
25 changed files with 637 additions and 47 deletions

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
module Nostr
# Base class for all signature validation errors
class SignatureValidationError < Error; end
end

View File

@ -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

67
lib/nostr/signature.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
module Nostr
class InvalidSignatureFormatError < SignatureValidationError
def initialize: -> void
end
end

View File

@ -0,0 +1,5 @@
module Nostr
class InvalidSignatureLengthError < SignatureValidationError
def initialize: -> void
end
end

View File

@ -0,0 +1,5 @@
module Nostr
class InvalidSignatureTypeError < SignatureValidationError
def initialize: -> void
end
end

View File

@ -0,0 +1,4 @@
module Nostr
class SignatureValidationError < Error
end
end

View File

@ -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]]

14
sig/nostr/signature.rbs Normal file
View File

@ -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

View File

@ -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

16
sig/vendor/schnorr/signature.rbs vendored Normal file
View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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