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:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
18
lib/nostr/errors/invalid_signature_format_error.rb
Normal file
18
lib/nostr/errors/invalid_signature_format_error.rb
Normal 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
|
||||
18
lib/nostr/errors/invalid_signature_length_error.rb
Normal file
18
lib/nostr/errors/invalid_signature_length_error.rb
Normal 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
|
||||
16
lib/nostr/errors/invalid_signature_type_error.rb
Normal file
16
lib/nostr/errors/invalid_signature_type_error.rb
Normal 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
|
||||
6
lib/nostr/errors/signature_validation_error.rb
Normal file
6
lib/nostr/errors/signature_validation_error.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Base class for all signature validation errors
|
||||
class SignatureValidationError < Error; end
|
||||
end
|
||||
@@ -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
67
lib/nostr/signature.rb
Normal 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
|
||||
Reference in New Issue
Block a user