wildcat-finance / v2-protocol

Other
1 stars 0 forks source link

Consider: Thoughts on MLA signing mechanism #41

Closed laurenceday closed 1 month ago

laurenceday commented 2 months ago

I've been thinking about a way to allow for MLAs to be signed in a privacy preserving manner without storing things like lender details on a website server.

One thing to bear in mind here is that some borrowers may not approve of the template MLA that we provide, so they'll want to be able to upload their own text.

I'd really like us to avoid having to implement a login system, both because of the time involved and because it'll be really clunky to do so.

My original idea was that we ask borrowers [whether they're using multisigs or not] to maintain a separate Ethereum address that they don't actually use, and use that as an asymmetric encryption key - the public key for that address is what's used to encrypt data submitted by lenders [name, address, jurisdiction etc], and the corresponding private key is used to decrypt it. It's simply a mechanism for the EOA to act as a password.

With that said, that's a royal ballache for people that aren't crypto-savvy, so we have an alternative: we don't even bother with an additional EOA and just ask borrowers to provide a password/memorable phrase, generate a public key from that in-browser and store that public key for MLA signing. When it comes to decrypting, they just enter the password (the seed) and it locally calculates the private key to decrypt. This could all be done in-browser.

Lenders signing MLAs would encrypt the data with that public key. What they would be encrypting is a hash of the text of the legal agreement they're signing, their wallet address, plus the plaintext of whatever fields they're being asked to submit. Encrypted data can either be stored directly within the site for the borrower to fetch, or uploaded to IPFS. They'd subsequently be given the option to download the text of the agreement they've just signed alongside the signature corresponding to what they've signed. We can render this stuff in PDF pretty easily if we want.

None of this is tricky cryptographically: Thom is likely to be able to do it once he's finished with the notification system.

It also means that we can support additional MLAs with minimal white-gloving: just uploading the text and providing a plaintext box for lenders to fill in the relevant details. Borrowers can subsequently check these if they wish and remove credentials of anyone who filled in obvious bullshit.

Will try graph this out soon.

Acceptance Criteria

Generated by Zenhub AI

laurenceday commented 2 months ago
image
laurenceday commented 2 months ago

I banged together a Python script that demonstrates this. We stretch a password into a seed that's used to generate an RSA keypair, store the public key for a borrower on the site and allow the lender to encrypt their response wih that key.

That response can be stored on the site too. Borrowers could decrypt data with the private key generated by recreating the seed from the password via a pseudorandom generator and fixed salt (using a PRNG means that all the entropy for the numbers used is generated from the password).

We'd lose a LOT of cryptographic security going the pseudorandom route, but it'd be less painful than demanding that borrowers store the RSA private key and provide that in-browser for decryption (although I think this IS probably best).

I can't see people mounting ASIC brute-force attacks to try and get the name of an Ethereum address, but it's worth being paranoid.

Both of these options are easier from a UX perspective than having a login.

If you want to run this locally, you'll first need to pip install cryptography. This is using a 'real' version of RSA rather than a pseudorandom one, FWIW.

import os
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization

# Fixed 32-bit (4-byte) salt
FIXED_SALT = b'\xde\xad\xbe\xef'  # Arbitrary 4-byte value

def derive_seed(password, salt, length=32):
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=length,
        salt=salt,
        iterations=100000,
    )
    return kdf.derive(password)

def generate_rsa_key(seed):
    # Use the seed to initialize the random number generator
    rsa_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=None
    )
    return rsa_key

def print_rsa_keys(private_key, public_key):
    # Print private key in PEM format
    private_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.NoEncryption()
    )
    print("Private Key (PEM format):")
    print(private_pem.decode())

    # Print public key in PEM format
    public_pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    print("Public Key (PEM format):")
    print(public_pem.decode())

# Step 1: User A inputs a password
print("Step 1: User A enters a password")
password = input("User A, please enter your password: ").encode('utf-8')

# Step 2: Derive a seed using PBKDF2 with the fixed salt
print("\nStep 2: Deriving a seed using PBKDF2 with fixed salt")
seed = derive_seed(password, FIXED_SALT)

print(f"Fixed Salt (hex): {FIXED_SALT.hex()}")
print(f"Derived seed (hex): {seed.hex()}")

# Step 3: Generate RSA key pair using the derived seed
print("\nStep 3: Generating RSA key pair using the derived seed")
private_key = generate_rsa_key(seed)
public_key = private_key.public_key()

# Print the generated RSA keys
print("\nGenerated RSA Keys:")
print_rsa_keys(private_key, public_key)

# Step 4: User B encrypts data
print("\nStep 4: User B encrypts data")
message = input("User B, enter the message to encrypt: ").encode('utf-8')

# Encrypt with RSA public key
ciphertext = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

print(f"Encrypted message (hex): {ciphertext.hex()}")

# Step 5: User A decrypts data
print("\nStep 5: User A decrypts data")

# User A enters password again for verification
while True:
    verify_password = input("User A, please enter your password again for verification: ").encode('utf-8')
    verify_seed = derive_seed(verify_password, FIXED_SALT)
    if verify_seed == seed:
        print("Password verified successfully.")
        break
    else:
        print("Incorrect password. Please try again.")

# Decrypt the message using the private key
decrypted_message = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

print(f"Decrypted message: {decrypted_message.decode('utf-8')}")

# Step 6: Verify that the decrypted message matches the original
print("\nStep 6: Verifying decrypted message")
if decrypted_message == message:
    print("Success: The decrypted message matches the original message.")
else:
    print("Error: The decrypted message does not match the original message.")

print("\nProcess completed successfully!")
laurenceday commented 1 month ago

Closing as not an active issue for the protocol itself, more an auxiliary UX concern