Hash functions and Signatures: Primitives for making a cryptocurrency.
Various considerations interweave into the creation of a cryptocurrency. some foundational primitives that center around cryptography are hash functions and digital signatures which are essential for securing transactions and protecting user privacy. this is going to be the main focus of this article.
What is a Hash function?
Hash functions are mathematical functions that transform a given data set into a bit string of fixed size, known also as the “hash value.”
Some features of a hash function are:
- Deterministic — Given a dataset of the same input should equal the same output. This property is crucial for consistency and reliability.
- Efficient — Any size input equals the same output. No matter the size of the data, the output stays the same size. Computational efficiency is necessary to save time and storage space.
- Unidirectional(Preimage resistance) — A hash function is a one-way function and as such you cannot use the output to find out the input.
given y, you can’t find any x such
that hash(x) == y
- Collision Resistance — It should be very unlikely for two unique inputs to produce the same hash value. Collisions do happen but a good hash function must fully minimize the risk of it ever happening.
- Unique — Each hash is practically unique to the data it’s generated from. This is based on the “Avalanche effect” which implies that when you change 1 bit of the input, about half the output bits should change.
What are Digital Signatures?
Digital signatures are one of the most basic foundational cryptographic tools used to provide key security features like legitimacy, unforgeability, and undeniability.
A digital signature is a cryptographic mechanism that verifies the authenticity and integrity of digital data. It is very similar to a digital version of a handwritten signature or a stamp used to show ownership and legitimacy.
Digital Signatures as regards cryptocurrency make use of three functions:
- GenerateKeys(): The
GenerateKeys()
function is responsible for generating a pair of cryptographic keys: a private key(secretKey) which is kept secret by the signer and used to create digital signatures and a public key which is freely shared with others and used to verify digital signatures. As explained above it’s practically impossible to derive the private key from the public key due to the hash algorithm.
// GenerateKeys generates a pair of RSA keys.
func GenerateKeys() (*rsa.PrivateKey, *rsa.PublicKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
return privateKey, &privateKey.PublicKey, nil
}
The above code snippet is written using the Go crypto/rsa
package. This package will be used for other examples as well.
2. Sign(secretKey, message): The Sign()
function takes as input the private key (secretKey
) and a message (message
) that the sender wants to sign. The process involves two main steps:
- Hashing: As explained above, the message is typically hashed using a cryptographic hash function to produce a fixed-size hash value. This hashed output serves as a condensed and unique representation of the original message.
- Encryption: The hash value is then encrypted with the sender’s private key, creating the digital signature. This encryption binds the signature to the specific message and private key.
// Sign creates a digital signature for a message using the private key.
func Sign(privateKey *rsa.PrivateKey, message []byte) ([]byte, error) {
hashed := sha256.Sum256(message)
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
if err != nil {
return nil, err
}
return signature, nil
}
3. Verify(publicKey, message, signature): The Verify()
function is used by the recipient to validate the authenticity and integrity of the message. It takes as input the public key (publicKey
), the original message (message
), and the digital signature (signature
). The verification process involves the following steps:
- Hashing: Similar to the signing process, the original message is hashed using the same cryptographic hash function.
- Decryption: The digital signature is decrypted using the sender’s public key. This should yield the original hash value if the signature is valid.
- Hash Comparison: The computed hash value from the decryption is compared with the hash value derived from the original message. If they match, the signature is considered valid (authenticity), indicating that the message has not been altered (integrity) and was indeed signed by the private key associated with the provided public key. The signer cannot deny having sent the message (non-repudiation).
// Verify checks the validity of a digital signature using the public key.
func Verify(publicKey *rsa.PublicKey, message, signature []byte) error {
hashed := sha256.Sum256(message)
err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], signature)
if err != nil {
return fmt.Errorf("signature verification failed: %v", err)
}
return nil
}
In conclusion, hash functions and digital signatures have a symbiotic relationship as they work hand in hand. The relationship between hash functions and digital signatures can be understood in terms of the roles each plays in ensuring the integrity, authenticity, and non-repudiation of digital data. Digital signatures rely on hash functions as they cannot function without a reliable hash function. There are different hash functions in existence each with its level of strength, efficiency, and security. Any weakness in the hash function can compromise the security of the entire signature system.
N.B: The code examples are based on the crypto/rsa
package from the Go standard library and is meant to bring the concept closer to home. Things are very much different in a real-world scenario as well-established cryptographic packages are utilized. For instance, the examples utilize the SHA-256 algorithm for hashing, which is not a bad choice in itself. However, the choice of the hash function depends on the security requirements and the specific use case.