2.2.5 Cycle 5 - Signatures and Key Generation

Design

Objectives

The next module that needs implementing and introducing to the code base is the cryptography side, this is the part of the code that allows a node to sign things and to validate other wallet's transactions and signatures.

This will be done through either the RSA or DSA algorithm, as these are the most common and well-known signature algorithms and I can be sure that they work without having to design and test my own signature algorithm.

Usability Features

  • The ability to generate keys as needed if the user doesn't have any to reduce friction.

  • Once key pair has been either inputted or generated the user shouldn't have to worry about them during the Node's run time.

Key Variables

Variable Name
Use

private_key

This is a private key that should not be shared and is used to sign data.

public_key

This is a publicly available key derived from the private key that can be used to validate that a specific private key produced a signature. Can also be used as an identifier for a wallet.

Choosing a Signature Algorithm

Before this part of the program can be designed and programmed, it is first required to choose a signature algorithm for generating signatures and validating transactions and therefore blocks.

The main two ways to do this are using either the RSA or DSA algorithms, both of which are considered safe and secure algorithms as of 2022, and can both be used for use case of which I will be using them for in this project.

Summary:

  • Primarily used for secure data transmission

  • Developed in 1977 by Ron Rivest, Adi Shamir and Leonard Adleman.

  • Uses the factorisation of product of two large primes for its mathematical security.

  • Faster Encryption than DSA

  • Slower Decryption than DSA

A basic diagram of how RSA works. (GeeksforGeeks, 2020)

Due to DSA being primarily used and tested for signature and verification thus having being tested for the use case of which I will be using it for and having faster decryption than RSA - which will be used a lot more than encryption during the project - I will be using the DSA algorithm for this project.

Choosing How to Implement The Algorithm

Now that the DSA algorithm has been selected it's time to decide how to implement it: either through a library or from scratch.

This is a choice made possible by the fact that the DSA algorithm is open source and built using entirely mathematical functions so should theoretically be able to be implemented into any language.

However after looking into how the DSA algorithm works and what the V Language can handle, an issue is uncovered, one of the ways in which DSA stays secure is through the use of massively large prime numbers - currently recommended to be 2048 bits in length - however due to how Vlang integrates variables, in order to reach this size the number needs to be split up into batches of byte long values which would make processing the data required to use DSA a lot more complicated than it already is.

To clarify this doesn't mean it is impossible to write your own copy of the DSA algorithm in Vlang, it's just a lot more complicated than it would be in most other languages and puts it outside the time scope for this project.

Therefore as the 'from-scratch' method is off the table due to the complexity and therefore time scope I will instead be implementing DSA using the prebuilt crypto.ed25519 library, which includes a generate, sign and validate function all built in, although some wrapper functions will still need to be created to handle the loading/saving of these keys and ensuring they supply the data needed in the program and not an optional return like they do currently.

Design

All that is required in this stage is to create some wrapper functions for the libraries pre-existing functions to ensure that the data supplied to the rest of the node is what we expect it to be, as well as a custom validation function which will be used to check that a public and private key pair match such that some data can be signed and then validated using that pair.

Pseudocode

Wrapper functions:

// Pseudocode

IMPORT crypto.ed25519 as dsa

FUNCTION gen_keys():
	TRY:
		// generate the keys using the dsa module
		public_key, private_key = dsa.generate_key()
		
		// validate the keys to check they were generated correctly
		validate_keys(public_key, private_key)
		
		RETURN public_key, private_key
	CATCH:	
		// if anything goes wrong in the "TRY" section, this will run
		OUTPUT "Error generating keys"
		EXIT
	END TRY	
END FUNCTION


FUNCTION sign(private_key, data):
	TRY:
		// sign the data supplied using the private key
		signature = dsa.sign(private_key, data)
		RETURN signature
	CATCH:
		OUTPUT "Error signing data"
		EXIT
	END TRY
END FUNCTION


FUNCTION verify(public_key, data, signature):
	TRY:
		// check if a public key matches the signature created from some data.
		verified = dsa.verify(public_key, data, signature)
		RETURN verified
	CATCH:
		OUTPUT "Error verifying data"
		EXIT
	END TRY	
END FUNCTION	

Validation function

// Pseudocode

FUNCTION validate_keys(public_key, private_key):
	// data is just some test binary data
	data = "Hello, world!".bytes()
	
	signature = sign(private_key, data)
	verified = verify(public_key, data, signature)
	
	IF (verified == FALSE):
		// Since private key pairs should only be used by the node itself
		// if something breaks then the node's keys are invalid.
		OUTPUT "Signature verification failed"
		EXIT
	ELSE:
		OUTPUT "Signature verified"
		RETURN TRUE
	END IF
END FUNCTION

Development

Converting these designs to actual code is actually pretty simple and is just a case of turning the pseudocode into something more resembling V code. The largest change here is converting from the 'try-catch' structure of error handling into V's 'or' based handling.

One other thing worth mentioning in this section is the use of error codes, if you look at any part of the code that includes a function called exit you will see a number being passed as a parameter, this number represents a specific type of error and can be looked up in the ExitCodes.txt file supplied with the distribution files.

For example, at this specific point in the project the 'Cryptography Errors' section within ExitCodes.txt is the following:

// Cyptography Errors
130 - Failed to validate keypair
140 - Error signing data
145 - Error validating data
150 - Error generating new keypair

Outcome

All the code in this cycle is within the file /packages/node/src/modules/cryptography/main.v found here.

Wrapper functions

// Vlang - src/modules/cryptography/main.v

module cryptography
import crypto.ed25519 as dsa

pub fn gen_keys() (dsa.PublicKey, dsa.PrivateKey) {
	public_key, private_key := dsa.generate_key() or {
		eprintln("Error generating keys")
		exit(150)
	}
	validate_keys(public_key, private_key)

	return public_key, private_key
}

pub fn sign(private_key dsa.PrivateKey, data []u8) []u8 {
	signature := dsa.sign(private_key, data) or {
		eprintln("Error signing data")
		exit(140)
	}
	return signature
}

pub fn verify(public_key dsa.PublicKey, data []u8, signature []u8) bool {
	verified := dsa.verify(public_key, data, signature) or {
		eprintln("Error verifying data")
		exit(145)
	}
	return verified
}	

Validation function

// Vlang - src/modules/cryptography/main.v

pub fn validate_keys(public_key dsa.PublicKey, private_key dsa.PrivateKey) bool {
	data := "Hello, world!".bytes()
	signature := sign(private_key, data)
	verified := verify(public_key, data, signature)
	if !verified {
		eprintln("Signature verification failed")
		exit(130)
	} else {
		println("Signature verified")
		return true
	}
}

Challenges

The main challenge in this cycle was the attempted creation of a custom built DSA algorithm without using the library that I ended up using. I didn't mention this much in the `Choosing How to Implement The Algorithm` as all the reasoning in that sector for choosing a prebuilt library based system is correct, it's just how I came to understand those reasons that are the challenge.

This included the initial setup for a key generator, a probable prime generator (used to generate large numbers that are probably prime numbers to a high degree of probability) and some other utility files.

To see all the code written, and eventually scrapped, visit the archive file here.

Testing

Tests

Test
Instructions
What I expect
What actually happens
Pass/Fail

1

Generate a key pair.

Keys to be generated and validated successfully without crashing.

As expected

Pass

2

Sign some random input data.

A signature to be generated without crashing.

As expected

Pass

3

Validate the data from test 2.

The data generated from test 2 should be validated successfully.

As expected

Pass

Evidence

Test 1 - Key generation tests

Test 2 - Key signature test

Test 3 - Key verification test

Last updated