This comprehensive guide explains everything you need to know about cryptographic signatures, digital authentication, and message verification within our blockchain-integrated financial platform.
A digital signature is a mathematical scheme that provides proof of:
Unlike handwritten signatures, digital signatures are cryptographically secure and mathematically verifiable.
Digital signatures rely on public-key cryptography:
Before signing, messages are processed through hash functions:
graph TB
A[Original Message] --> B[Hash Function SHA-256]
B --> C[Message Hash]
C --> D[Sign with Private Key]
D --> E[Digital Signature]
F[Original Message] --> G[Hash Function SHA-256]
G --> H[Message Hash]
H --> I[Verify with Public Key + Signature]
E --> I
I --> J[Valid/Invalid]
| Aspect | Physical Signature | Digital Signature |
|---|---|---|
| Security | Easily forged | Cryptographically secure |
| Verification | Subjective comparison | Mathematical verification |
| Binding | Document-specific | Content-specific |
| Integrity | No tamper detection | Detects any changes |
| Scalability | Manual process | Automated verification |
| Legal Status | Widely accepted | Legally recognized in most jurisdictions |
// Example message to be signed
const message = "Transfer 1000 USDC to contractor 0x742d35...";
const timestamp = Date.now();
const nonce = generateRandomNonce();
// Create structured message
const structuredMessage = {
action: "payment",
amount: "1000000000", // 1000 USDC in smallest units
recipient: "0x742d35Cc6535C4532CF81d828c3e7c8e89b92a0B",
timestamp: timestamp,
nonce: nonce
};
const messageString = JSON.stringify(structuredMessage);
// Generate cryptographic hash
const crypto = require('crypto');
function generateHash(message) {
return crypto
.createHash('sha256')
.update(message, 'utf8')
.digest('hex');
}
const messageHash = generateHash(messageString);
console.log('Message Hash:', messageHash);
// Output: a8b2c3d4e5f6... (64-character hex string)
// Sign the hash with private key (using elliptic curve cryptography)
const EC = require('elliptic').ec;
const ec = new EC('secp256k1'); // Same curve as Bitcoin/Ethereum
function signMessage(messageHash, privateKey) {
const keyPair = ec.keyFromPrivate(privateKey, 'hex');
const signature = keyPair.sign(messageHash, 'hex', { canonical: true });
return {
r: signature.r.toString('hex'),
s: signature.s.toString('hex'),
v: signature.recoveryParam
};
}
const privateKey = "your-private-key-here"; // Never expose this!
const signature = signMessage(messageHash, privateKey);
console.log('Signature:', signature);
// Recipient receives original message and signature
function verifySignature(originalMessage, signature, publicKey) {
// Regenerate hash from original message
const regeneratedHash = generateHash(originalMessage);
return verifyHashSignature(regeneratedHash, signature, publicKey);
}
function verifyHashSignature(messageHash, signature, publicKey) {
try {
const keyPair = ec.keyFromPublic(publicKey, 'hex');
const signatureObj = {
r: signature.r,
s: signature.s
};
// Verify signature mathematically
const isValid = keyPair.verify(messageHash, signatureObj);
return isValid;
} catch (error) {
console.error('Verification failed:', error);
return false;
}
}
// Complete verification process
const originalMessage = JSON.stringify(structuredMessage);
const isSignatureValid = verifySignature(originalMessage, signature, publicKey);
console.log('Signature Valid:', isSignatureValid);
// Additional checks
const messageIntegrity = generateHash(originalMessage) === messageHash;
const timeCheck = (Date.now() - structuredMessage.timestamp) < 300000; // 5 minutes
console.log('Message Integrity:', messageIntegrity);
console.log('Within Time Window:', timeCheck);
Our platform uses ECDSA with the secp256k1 curve, which provides:
Signature Components:
- r: x-coordinate of ephemeral key point on curve
- s: signature proof value computed using private key
- v: recovery parameter (helps derive public key from signature)
Verification Equation:
Point = (s^-1 * hash * G) + (s^-1 * r * PublicKey)
Where:
- G = Generator point on elliptic curve
- hash = SHA-256 hash of message
- PublicKey = Signer's public key point
// MetaMask message signing
async function signAuthenticationMessage(userAddress) {
const challengeMessage = `
Please sign this message to authenticate with Contracting.App
Address: ${userAddress}
Timestamp: ${Date.now()}
Nonce: ${generateNonce()}
This request will not trigger a blockchain transaction or cost any gas fees.
`;
try {
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [challengeMessage, userAddress]
});
return {
message: challengeMessage,
signature: signature,
address: userAddress
};
} catch (error) {
console.error('Message signing failed:', error);
throw error;
}
}
# Python backend verification
from eth_account.messages import encode_defunct
from eth_account import Account
import json
def verify_message_signature(message, signature, expected_address):
try:
# Encode message for verification
encoded_message = encode_defunct(text=message)
# Recover address from signature
recovered_address = Account.recover_message(
encoded_message,
signature=signature
)
# Check if recovered address matches expected
is_valid = recovered_address.lower() == expected_address.lower()
return {
'valid': is_valid,
'recovered_address': recovered_address,
'expected_address': expected_address
}
except Exception as e:
return {
'valid': False,
'error': str(e)
}
# Usage example
verification_result = verify_message_signature(
message="Please sign this message...",
signature="0x1234...",
expected_address="0x742d35..."
)
// Example transaction object
const transactionData = {
to: "0x...", // Contract or recipient address
value: "0", // ETH amount (usually 0 for contract calls)
data: "0x...", // Encoded function call data
gasLimit: "100000", // Maximum gas to use
gasPrice: "20000000000", // Gas price in wei
nonce: 42, // Transaction sequence number
chainId: 1329 // SEI Mainnet chain ID
};
// Sign transaction
async function signTransaction(transactionData, privateKey) {
const web3 = new Web3();
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
const signedTransaction = await account.signTransaction(transactionData);
return signedTransaction;
}
// Encode smart contract function call
const Web3 = require('web3');
const web3 = new Web3();
function encodeFunctionCall(contractABI, functionName, parameters) {
const contract = new web3.eth.Contract(contractABI);
const encodedData = contract.methods[functionName](...parameters).encodeABI();
return encodedData;
}
// Example: Encode milestone approval function
const approvalData = encodeFunctionCall(
MilestoneManagerABI,
'approveMilestone',
['milestone_123456']
);
console.log('Encoded function call:', approvalData);
// Output: 0xa1b2c3d4...
EIP-712 provides a standard for signing structured data that's both human-readable and secure.
// EIP-712 domain separator
const domain = {
name: 'Contracting.App',
version: '1',
chainId: 1329, // SEI Mainnet
verifyingContract: '0x...' // Contract address
};
// Define data types
const types = {
PaymentAuthorization: [
{ name: 'recipient', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'token', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
};
// Message data
const message = {
recipient: '0x742d35Cc6535C4532CF81d828c3e7c8e89b92a0B',
amount: '1000000000000000000000', // 1000 tokens
token: '0x...', // USDC contract address
nonce: 1,
deadline: Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
};
// Sign structured data
async function signStructuredData(domain, types, message) {
const data = {
types: types,
domain: domain,
primaryType: 'PaymentAuthorization',
message: message
};
try {
const signature = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [userAddress, JSON.stringify(data)]
});
return signature;
} catch (error) {
console.error('Structured data signing failed:', error);
throw error;
}
}
// Solidity contract for EIP-712 verification
contract PaymentVerifier {
using ECDSA for bytes32;
bytes32 private constant DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("Contracting.App")),
keccak256(bytes("1")),
1329, // SEI Mainnet
address(this)
)
);
bytes32 private constant PAYMENT_AUTHORIZATION_TYPEHASH = keccak256(
"PaymentAuthorization(address recipient,uint256 amount,address token,uint256 nonce,uint256 deadline)"
);
mapping(address => uint256) public nonces;
function verifyPaymentAuthorization(
address recipient,
uint256 amount,
address token,
uint256 nonce,
uint256 deadline,
bytes memory signature
) public view returns (address signer) {
require(block.timestamp <= deadline, "Signature expired");
bytes32 structHash = keccak256(
abi.encode(
PAYMENT_AUTHORIZATION_TYPEHASH,
recipient,
amount,
token,
nonce,
deadline
)
);
bytes32 hash = keccak256(
abi.encodePacked("\\x19\\x01", DOMAIN_SEPARATOR, structHash)
);
signer = hash.recover(signature);
require(nonces[signer] == nonce, "Invalid nonce");
}
function executePaymentWithSignature(
address recipient,
uint256 amount,
address token,
uint256 nonce,
uint256 deadline,
bytes memory signature
) external {
address signer = verifyPaymentAuthorization(
recipient, amount, token, nonce, deadline, signature
);
// Increment nonce to prevent replay attacks
nonces[signer]++;
// Execute payment logic
_executePayment(signer, recipient, amount, token);
}
}
class MultiSignatureManager {
constructor(contractAddress, web3Provider) {
this.contractAddress = contractAddress;
this.web3 = web3Provider;
this.contract = new this.web3.eth.Contract(MultiSigABI, contractAddress);
}
async createProposal(transactionData, description) {
const proposalId = this.generateProposalId();
const proposal = {
id: proposalId,
to: transactionData.to,
value: transactionData.value,
data: transactionData.data,
description: description,
requiredSignatures: await this.getRequiredSignatures(),
signatures: [],
status: 'PENDING',
createdAt: Date.now(),
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000) // 7 days
};
// Submit proposal to contract
const tx = await this.contract.methods.submitTransaction(
proposal.to,
proposal.value,
proposal.data
).send({ from: this.currentUser });
proposal.transactionHash = tx.transactionHash;
return proposal;
}
async signProposal(proposalId) {
try {
// Get proposal details
const proposal = await this.getProposal(proposalId);
// Create signature for proposal
const messageHash = this.web3.utils.soliditySha3(
{ t: 'bytes32', v: proposalId },
{ t: 'address', v: proposal.to },
{ t: 'uint256', v: proposal.value },
{ t: 'bytes', v: proposal.data }
);
const signature = await this.web3.eth.personal.sign(
messageHash,
this.currentUser
);
// Submit signature to contract
const tx = await this.contract.methods.confirmTransaction(
proposalId
).send({ from: this.currentUser });
// Check if enough signatures collected
const signatureCount = await this.contract.methods.getConfirmationCount(
proposalId
).call();
if (signatureCount >= proposal.requiredSignatures) {
// Execute transaction automatically
await this.executeProposal(proposalId);
}
return {
proposalId,
signature,
transactionHash: tx.transactionHash,
signatureCount: signatureCount,
readyForExecution: signatureCount >= proposal.requiredSignatures
};
} catch (error) {
console.error('Multi-sig signing failed:', error);
throw error;
}
}
async executeProposal(proposalId) {
const tx = await this.contract.methods.executeTransaction(
proposalId
).send({
from: this.currentUser,
gasLimit: 500000
});
return tx;
}
}
// Frontend authentication flow
class AuthenticationManager {
async authenticate(walletAddress) {
try {
// Step 1: Request challenge from server
const challengeResponse = await fetch('/api/auth/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: walletAddress })
});
const challengeData = await challengeResponse.json();
// Step 2: Sign challenge with wallet
const signatureData = await this.signChallenge(
challengeData.challenge,
walletAddress
);
// Step 3: Submit signature for verification
const authResponse = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address: walletAddress,
challenge: challengeData.challenge,
signature: signatureData.signature,
walletType: signatureData.walletType
})
});
if (authResponse.ok) {
const authResult = await authResponse.json();
this.setAuthToken(authResult.token);
return authResult;
} else {
throw new Error('Authentication failed');
}
} catch (error) {
console.error('Authentication error:', error);
throw error;
}
}
async signChallenge(challenge, address) {
// Determine wallet type and sign accordingly
if (window.ethereum && window.ethereum.isMetaMask) {
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [challenge, address]
});
return { signature, walletType: 'metamask' };
} else if (window.compass) {
const signature = await window.compass.signMessage(challenge);
return { signature, walletType: 'compass' };
} else {
throw new Error('No supported wallet found');
}
}
setAuthToken(token) {
localStorage.setItem('authToken', token);
// Set authorization header for future requests
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
}
# Python Flask backend verification
from flask import Flask, request, jsonify
from eth_account.messages import encode_defunct
from eth_account import Account
import jwt
import time
import secrets
app = Flask(__name__)
class AuthenticationService:
def __init__(self):
self.active_challenges = {}
self.jwt_secret = os.environ.get('JWT_SECRET')
def generate_challenge(self, address):
"""Generate a unique challenge for wallet authentication"""
nonce = secrets.token_hex(16)
timestamp = int(time.time())
challenge = f"""
Please sign this message to authenticate with Contracting.App
Address: {address}
Timestamp: {timestamp}
Nonce: {nonce}
This request will not trigger a blockchain transaction.
"""
# Store challenge temporarily (expires in 5 minutes)
challenge_key = f"{address}:{nonce}"
self.active_challenges[challenge_key] = {
'challenge': challenge,
'timestamp': timestamp,
'expires_at': timestamp + 300 # 5 minutes
}
return {
'challenge': challenge,
'nonce': nonce,
'expires_at': timestamp + 300
}
def verify_signature(self, address, challenge, signature):
"""Verify the signature against the challenge"""
try:
# Find matching challenge
challenge_data = None
for key, data in self.active_challenges.items():
if data['challenge'] == challenge:
challenge_data = data
challenge_key = key
break
if not challenge_data:
return {'valid': False, 'error': 'Challenge not found'}
# Check if challenge has expired
if time.time() > challenge_data['expires_at']:
del self.active_challenges[challenge_key]
return {'valid': False, 'error': 'Challenge expired'}
# Verify signature
encoded_message = encode_defunct(text=challenge)
recovered_address = Account.recover_message(
encoded_message,
signature=signature
)
is_valid = recovered_address.lower() == address.lower()
if is_valid:
# Remove challenge (prevent replay attacks)
del self.active_challenges[challenge_key]
# Generate JWT token
token = self.generate_jwt_token(address)
return {
'valid': True,
'address': recovered_address,
'token': token
}
else:
return {
'valid': False,
'error': 'Signature verification failed'
}
except Exception as e:
return {'valid': False, 'error': str(e)}
def generate_jwt_token(self, address):
"""Generate JWT token for authenticated session"""
payload = {
'address': address,
'iat': time.time(),
'exp': time.time() + (24 * 60 * 60) # 24 hours
}
token = jwt.encode(payload, self.jwt_secret, algorithm='HS256')
return token
auth_service = AuthenticationService()
@app.route('/api/auth/challenge', methods=['POST'])
def generate_auth_challenge():
data = request.json
address = data.get('address')
if not address:
return jsonify({'error': 'Address required'}), 400
challenge_data = auth_service.generate_challenge(address)
return jsonify(challenge_data)
@app.route('/api/auth/verify', methods=['POST'])
def verify_auth_signature():
data = request.json
address = data.get('address')
challenge = data.get('challenge')
signature = data.get('signature')
if not all([address, challenge, signature]):
return jsonify({'error': 'Missing required fields'}), 400
verification_result = auth_service.verify_signature(address, challenge, signature)
if verification_result['valid']:
return jsonify({
'success': True,
'token': verification_result['token'],
'address': verification_result['address']
})
else:
return jsonify({
'success': False,
'error': verification_result['error']
}), 401
// Document integrity verification through signatures
class DocumentSignatureManager {
async signDocument(documentFile, signerAddress) {
try {
// Step 1: Generate document hash
const documentHash = await this.generateDocumentHash(documentFile);
// Step 2: Create signature metadata
const signatureMetadata = {
documentHash: documentHash,
fileName: documentFile.name,
fileSize: documentFile.size,
contentType: documentFile.type,
timestamp: Date.now(),
signer: signerAddress
};
// Step 3: Sign the metadata
const metadataString = JSON.stringify(signatureMetadata);
const signature = await this.signMessage(metadataString, signerAddress);
// Step 4: Upload to platform with signature
const signedDocument = {
file: documentFile,
metadata: signatureMetadata,
signature: signature
};
const uploadResult = await this.uploadSignedDocument(signedDocument);
return {
documentId: uploadResult.documentId,
documentHash: documentHash,
signature: signature,
ipfsHash: uploadResult.ipfsHash,
verificationUrl: uploadResult.verificationUrl
};
} catch (error) {
console.error('Document signing failed:', error);
throw error;
}
}
async generateDocumentHash(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (event) => {
try {
const arrayBuffer = event.target.result;
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
resolve('0x' + hashHex);
} catch (error) {
reject(error);
}
};
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsArrayBuffer(file);
});
}
async verifyDocumentSignature(documentId) {
try {
// Fetch document metadata and signature
const documentData = await fetch(`/api/documents/${documentId}/verification`);
const verificationData = await documentData.json();
// Download original document
const documentFile = await fetch(verificationData.downloadUrl);
const documentBlob = await documentFile.blob();
// Regenerate hash
const regeneratedHash = await this.generateDocumentHash(documentBlob);
// Verify signature
const isSignatureValid = await this.verifyMessageSignature(
verificationData.signedMetadata,
verificationData.signature,
verificationData.signer
);
// Check hash integrity
const isHashValid = regeneratedHash === verificationData.originalHash;
return {
documentId: documentId,
signatureValid: isSignatureValid,
hashValid: isHashValid,
overallValid: isSignatureValid && isHashValid,
signer: verificationData.signer,
signedAt: verificationData.timestamp,
verification: {
originalHash: verificationData.originalHash,
regeneratedHash: regeneratedHash,
hashMatch: isHashValid
}
};
} catch (error) {
console.error('Document verification failed:', error);
throw error;
}
}
}
// Smart contract for signature verification
contract SignatureVerifier {
using ECDSA for bytes32;
struct SignedDocument {
bytes32 documentHash;
address signer;
uint256 timestamp;
string ipfsHash;
bool isValid;
}
mapping(bytes32 => SignedDocument) public documents;
mapping(address => uint256) public signerNonces;
event DocumentSigned(
bytes32 indexed documentHash,
address indexed signer,
string ipfsHash,
uint256 timestamp
);
event SignatureVerified(
bytes32 indexed documentHash,
address indexed verifier,
bool isValid
);
function signDocument(
bytes32 documentHash,
string memory ipfsHash,
bytes memory signature
) external {
// Verify signature
bytes32 messageHash = keccak256(abi.encodePacked(
"\\x19Ethereum Signed Message:\\n32",
documentHash
));
address recoveredSigner = messageHash.recover(signature);
require(recoveredSigner == msg.sender, "Invalid signature");
// Store signed document
documents[documentHash] = SignedDocument({
documentHash: documentHash,
signer: msg.sender,
timestamp: block.timestamp,
ipfsHash: ipfsHash,
isValid: true
});
emit DocumentSigned(documentHash, msg.sender, ipfsHash, block.timestamp);
}
function verifyDocumentSignature(
bytes32 documentHash
) external view returns (
address signer,
uint256 timestamp,
string memory ipfsHash,
bool isValid
) {
SignedDocument memory doc = documents[documentHash];
require(doc.signer != address(0), "Document not found");
return (doc.signer, doc.timestamp, doc.ipfsHash, doc.isValid);
}
function batchVerifySignatures(
bytes32[] memory documentHashes
) external view returns (bool[] memory results) {
results = new bool[](documentHashes.length);
for (uint i = 0; i < documentHashes.length; i++) {
SignedDocument memory doc = documents[documentHashes[i]];
results[i] = doc.isValid && doc.signer != address(0);
}
return results;
}
function revokeSignature(bytes32 documentHash) external {
SignedDocument storage doc = documents[documentHash];
require(doc.signer == msg.sender, "Only signer can revoke");
require(doc.isValid, "Already revoked");
doc.isValid = false;
emit SignatureVerified(documentHash, msg.sender, false);
}
}
Problem: Reusing valid signatures for unintended purposes Solution: Include nonces and timestamps in signed messages
// Secure message format with replay protection
function createSecureMessage(action, data, userAddress) {
const nonce = generateNonce();
const timestamp = Date.now();
const secureMessage = {
action: action,
data: data,
signer: userAddress,
nonce: nonce,
timestamp: timestamp,
expiresAt: timestamp + (5 * 60 * 1000) // 5 minutes
};
return JSON.stringify(secureMessage);
}
// Backend verification with replay protection
function verifySecureMessage(signedMessage, signature, expectedSigner) {
const messageData = JSON.parse(signedMessage);
// Check timestamp (prevent old messages)
if (Date.now() > messageData.expiresAt) {
throw new Error('Message expired');
}
// Check nonce (prevent replay)
if (usedNonces.has(messageData.nonce)) {
throw new Error('Nonce already used');
}
// Verify signature
const isValid = verifySignature(signedMessage, signature, expectedSigner);
if (isValid) {
// Mark nonce as used
usedNonces.add(messageData.nonce);
return true;
}
return false;
}
Problem: Multiple valid signatures for the same message Solution: Use canonical signature formats
// Ensure canonical signature format
function canonicalizeSignature(signature) {
const { r, s, v } = signature;
// Ensure 's' is in lower half of curve order
const secp256k1N = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
const sValue = BigInt('0x' + s);
if (sValue > secp256k1N / 2n) {
const canonicalS = (secp256k1N - sValue).toString(16).padStart(64, '0');
const canonicalV = v === 27 ? 28 : 27; // Flip recovery parameter
return { r, s: canonicalS, v: canonicalV };
}
return signature;
}
Problem: Compromise of signing keys Prevention Strategies:
// Secure key management practices
const securityBestPractices = {
keyGeneration: {
useHardwareWallets: "For large amounts and critical operations",
useSecureRandom: "Cryptographically secure random number generation",
avoidPredictablePatterns: "Never use simple patterns or dictionary words"
},
keyStorage: {
neverStoreOnline: "Keep private keys offline when possible",
useEncryption: "Encrypt keys if must be stored digitally",
useMultipleLocations: "Redundant secure storage locations",
regularBackups: "Test backup and recovery procedures"
},
keyUsage: {
minimizeExposure: "Use keys only when necessary",
useSecureEnvironments: "Sign on secure, malware-free devices",
verifyTransactions: "Always review what you're signing",
implementTimeouts: "Automatic logout and key clearing"
}
};
class SignatureValidator {
constructor() {
this.usedNonces = new Set();
this.maxMessageAge = 5 * 60 * 1000; // 5 minutes
this.trustedContracts = new Set([
'0x0165878A594ca255338adfa4d48449f69242Eb8F',
'0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9'
]);
}
async validateSignature(messageData, signature, expectedSigner) {
const validation = {
isValid: false,
errors: [],
warnings: []
};
try {
// Parse message data
const message = typeof messageData === 'string'
? JSON.parse(messageData)
: messageData;
// 1. Check message format
if (!this.validateMessageFormat(message)) {
validation.errors.push('Invalid message format');
return validation;
}
// 2. Check timestamp
const now = Date.now();
if (now > message.expiresAt) {
validation.errors.push('Message expired');
return validation;
}
if (message.timestamp > now + 60000) { // Allow 1 minute clock skew
validation.errors.push('Message timestamp in future');
return validation;
}
// 3. Check nonce
if (this.usedNonces.has(message.nonce)) {
validation.errors.push('Nonce already used (replay attack)');
return validation;
}
// 4. Verify signature format
if (!this.validateSignatureFormat(signature)) {
validation.errors.push('Invalid signature format');
return validation;
}
// 5. Verify cryptographic signature
const messageString = JSON.stringify(message);
const recoveredAddress = await this.recoverSignerAddress(messageString, signature);
if (recoveredAddress.toLowerCase() !== expectedSigner.toLowerCase()) {
validation.errors.push('Signature verification failed');
return validation;
}
// 6. Additional security checks
if (message.action === 'CONTRACT_INTERACTION' &&
!this.trustedContracts.has(message.data.contractAddress)) {
validation.warnings.push('Interaction with untrusted contract');
}
// Mark as valid
validation.isValid = true;
this.usedNonces.add(message.nonce);
// Cleanup old nonces periodically
if (this.usedNonces.size > 10000) {
this.cleanupOldNonces();
}
} catch (error) {
validation.errors.push(`Validation error: ${error.message}`);
}
return validation;
}
validateMessageFormat(message) {
const requiredFields = ['action', 'timestamp', 'nonce', 'expiresAt'];
return requiredFields.every(field => message.hasOwnProperty(field));
}
validateSignatureFormat(signature) {
// Check if signature has proper format (0x + 130 hex characters)
const sigRegex = /^0x[0-9a-fA-F]{130}$/;
return sigRegex.test(signature);
}
async recoverSignerAddress(message, signature) {
const messageHash = this.web3.utils.keccak256(message);
const recoveredAddress = await this.web3.eth.accounts.recover(messageHash, signature);
return recoveredAddress;
}
cleanupOldNonces() {
// In production, implement more sophisticated cleanup
// based on timestamp tracking
this.usedNonces.clear();
}
}
For enterprise deployments, consider HSM integration for critical signing operations:
// HSM integration for enterprise security
class HSMSignatureManager {
constructor(hsmConfig) {
this.hsmClient = new HSMClient(hsmConfig);
this.keyLabels = new Map();
}
async generateSecureKeyPair(keyLabel) {
try {
const keyPair = await this.hsmClient.generateKeyPair({
keyType: 'ECDSA',
curve: 'secp256k1',
label: keyLabel,
extractable: false, // Key cannot be extracted from HSM
signOnly: true
});
this.keyLabels.set(keyLabel, keyPair.publicKey);
return {
keyLabel: keyLabel,
publicKey: keyPair.publicKey,
keyId: keyPair.keyId
};
} catch (error) {
console.error('HSM key generation failed:', error);
throw error;
}
}
async signWithHSM(message, keyLabel) {
try {
const signature = await this.hsmClient.sign({
data: message,
keyLabel: keyLabel,
algorithm: 'ECDSA-SHA256'
});
return signature;
} catch (error) {
console.error('HSM signing failed:', error);
throw error;
}
}
async verifyHSMSignature(message, signature, keyLabel) {
try {
const publicKey = this.keyLabels.get(keyLabel);
if (!publicKey) {
throw new Error('Key not found');
}
const isValid = await this.hsmClient.verify({
data: message,
signature: signature,
publicKey: publicKey,
algorithm: 'ECDSA-SHA256'
});
return isValid;
} catch (error) {
console.error('HSM verification failed:', error);
throw error;
}
}
}
// Contractor milestone submission with signature
async function submitMilestoneWithSignature(projectId, milestoneData) {
const submissionData = {
action: 'MILESTONE_SUBMISSION',
projectId: projectId,
milestoneId: milestoneData.id,
completionPercentage: milestoneData.percentage,
documents: milestoneData.documents,
notes: milestoneData.notes,
timestamp: Date.now(),
nonce: generateNonce()
};
// Sign the submission
const signature = await signMessage(
JSON.stringify(submissionData),
contractorAddress
);
// Submit to blockchain
const tx = await milestoneContract.methods.submitMilestone(
submissionData.milestoneId,
submissionData.completionPercentage,
signature
).send({ from: contractorAddress });
return {
transactionHash: tx.transactionHash,
signedData: submissionData,
signature: signature
};
}
// Multi-party payment authorization
class PaymentAuthorizationWorkflow {
async initiatePayment(paymentData, requiredSigners) {
const authorizationRequest = {
action: 'PAYMENT_AUTHORIZATION',
amount: paymentData.amount,
recipient: paymentData.recipient,
projectId: paymentData.projectId,
milestoneId: paymentData.milestoneId,
requiredSigners: requiredSigners,
createdAt: Date.now(),
expiresAt: Date.now() + (24 * 60 * 60 * 1000), // 24 hours
nonce: generateNonce()
};
const authorizationId = this.generateAuthorizationId();
// Store authorization request
await this.storeAuthorizationRequest(authorizationId, authorizationRequest);
// Notify required signers
await this.notifySigners(requiredSigners, authorizationId);
return {
authorizationId,
requiredSignatures: requiredSigners.length,
expiresAt: authorizationRequest.expiresAt
};
}
async authorizePayment(authorizationId, signerAddress) {
// Get authorization request
const authRequest = await this.getAuthorizationRequest(authorizationId);
// Verify signer is authorized
if (!authRequest.requiredSigners.includes(signerAddress)) {
throw new Error('Unauthorized signer');
}
// Check if already signed
const existingSignatures = await this.getSignatures(authorizationId);
if (existingSignatures.some(sig => sig.signer === signerAddress)) {
throw new Error('Already signed by this address');
}
// Create and verify signature
const signature = await this.signAuthorizationRequest(authRequest, signerAddress);
// Store signature
await this.storeSignature(authorizationId, {
signer: signerAddress,
signature: signature,
signedAt: Date.now()
});
// Check if all signatures collected
const allSignatures = await this.getSignatures(authorizationId);
if (allSignatures.length >= authRequest.requiredSigners.length) {
// Execute payment
await this.executeAuthorizedPayment(authorizationId, authRequest, allSignatures);
}
return {
authorizationId,
signaturesCollected: allSignatures.length,
signaturesRequired: authRequest.requiredSigners.length,
readyForExecution: allSignatures.length >= authRequest.requiredSigners.length
};
}
}
// Construction document verification system
class DocumentIntegrityManager {
async submitVerifiedDocument(documentFile, projectId, documentType) {
try {
// 1. Generate document hash
const documentHash = await this.hashFile(documentFile);
// 2. Create verification metadata
const verificationData = {
action: 'DOCUMENT_VERIFICATION',
projectId: projectId,
documentType: documentType,
documentHash: documentHash,
fileName: documentFile.name,
fileSize: documentFile.size,
timestamp: Date.now(),
nonce: generateNonce()
};
// 3. Sign verification data
const signature = await this.signVerificationData(verificationData);
// 4. Upload to IPFS
const ipfsHash = await this.uploadToIPFS(documentFile);
// 5. Store on blockchain
const tx = await this.documentContract.methods.submitDocument(
documentHash,
ipfsHash,
JSON.stringify(verificationData),
signature
).send({ from: this.currentUser });
return {
documentHash: documentHash,
ipfsHash: ipfsHash,
transactionHash: tx.transactionHash,
verificationSignature: signature,
submittedAt: Date.now()
};
} catch (error) {
console.error('Document verification failed:', error);
throw error;
}
}
async verifyDocumentIntegrity(documentHash) {
try {
// 1. Get document data from blockchain
const blockchainData = await this.documentContract.methods
.getDocument(documentHash)
.call();
// 2. Download document from IPFS
const documentBlob = await this.downloadFromIPFS(blockchainData.ipfsHash);
// 3. Regenerate hash
const regeneratedHash = await this.hashFile(documentBlob);
// 4. Verify signature
const verificationData = JSON.parse(blockchainData.metadata);
const isSignatureValid = await this.verifySignature(
blockchainData.metadata,
blockchainData.signature,
blockchainData.signer
);
return {
documentHash: documentHash,
originalHash: blockchainData.documentHash,
regeneratedHash: regeneratedHash,
hashMatch: documentHash === regeneratedHash,
signatureValid: isSignatureValid,
signer: blockchainData.signer,
submittedAt: blockchainData.timestamp,
ipfsHash: blockchainData.ipfsHash,
overallValid: (documentHash === regeneratedHash) && isSignatureValid
};
} catch (error) {
console.error('Document verification failed:', error);
throw error;
}
}
}
// React component for document signing
import React, { useState } from 'react';
import { useWallet } from '../hooks/useWallet';
const DocumentSigner = ({ projectId }) => {
const [selectedFile, setSelectedFile] = useState(null);
const [signing, setSigning] = useState(false);
const [signatureResult, setSignatureResult] = useState(null);
const { address, signMessage, isConnected } = useWallet();
const handleFileSelect = (event) => {
const file = event.target.files[0];
if (file) {
setSelectedFile(file);
}
};
const signDocument = async () => {
if (!selectedFile || !isConnected) return;
setSigning(true);
try {
// Create document integrity manager
const docManager = new DocumentIntegrityManager();
// Sign and submit document
const result = await docManager.submitVerifiedDocument(
selectedFile,
projectId,
'blueprint' // or other document type
);
setSignatureResult(result);
// Show success message
toast.success('Document signed and submitted successfully!');
} catch (error) {
console.error('Document signing failed:', error);
toast.error('Failed to sign document: ' + error.message);
} finally {
setSigning(false);
}
};
return (
<div className="document-signer">
<h3>Sign and Submit Document</h3>
<div className="file-input">
<input
type="file"
onChange={handleFileSelect}
accept=".pdf,.doc,.docx,.jpg,.png"
/>
</div>
{selectedFile && (
<div className="file-info">
<p><strong>File:</strong> {selectedFile.name}</p>
<p><strong>Size:</strong> {(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p>
<p><strong>Type:</strong> {selectedFile.type}</p>
</div>
)}
<button
onClick={signDocument}
disabled={!selectedFile || !isConnected || signing}
className="sign-button"
>
{signing ? 'Signing...' : 'Sign & Submit Document'}
</button>
{signatureResult && (
<div className="signature-result">
<h4>Document Successfully Signed!</h4>
<p><strong>Document Hash:</strong> {signatureResult.documentHash}</p>
<p><strong>IPFS Hash:</strong> {signatureResult.ipfsHash}</p>
<p><strong>Transaction:</strong>
<a href={`https://seitrace.com/tx/${signatureResult.transactionHash}`}
target="_blank" rel="noopener noreferrer">
View on Explorer
</a>
</p>
</div>
)}
</div>
);
};
export default DocumentSigner;
For privacy-preserving authentication where you need to prove knowledge of a secret without revealing it:
// Zero-knowledge signature scheme
class ZKSignatureManager {
constructor() {
this.curve = new elliptic.ec('secp256k1');
}
// Generate commitment for ZK proof
generateCommitment(secret, nonce) {
const commitment = this.curve.g.mul(secret).add(this.curve.g.mul(nonce));
return commitment;
}
// Create ZK proof of signature knowledge
async createZKProofOfSignature(message, signature, privateKey) {
const messageHash = crypto.createHash('sha256').update(message).digest('hex');
// Generate random nonce for proof
const nonce = crypto.randomBytes(32);
// Create commitment
const commitment = this.generateCommitment(privateKey, nonce);
// Generate challenge
const challenge = crypto.createHash('sha256')
.update(commitment.encode('hex'))
.update(messageHash)
.digest('hex');
// Generate response
const response = BigInt('0x' + nonce.toString('hex')) +
BigInt('0x' + challenge) * BigInt('0x' + privateKey.toString('hex'));
return {
commitment: commitment.encode('hex'),
challenge: challenge,
response: response.toString(16),
messageHash: messageHash
};
}
// Verify ZK proof
verifyZKProof(proof, publicKey, message) {
const messageHash = crypto.createHash('sha256').update(message).digest('hex');
if (messageHash !== proof.messageHash) {
return false;
}
// Reconstruct commitment
const leftSide = this.curve.g.mul(BigInt('0x' + proof.response));
const rightSide = this.curve.decodePoint(proof.commitment, 'hex')
.add(this.curve.keyFromPublic(publicKey, 'hex').getPublic().mul(BigInt('0x' + proof.challenge)));
return leftSide.eq(rightSide);
}
}
For scenarios requiring t-of-n signatures:
// Threshold signature implementation
class ThresholdSignatureManager {
constructor(threshold, totalSigners) {
this.threshold = threshold;
this.totalSigners = totalSigners;
this.shares = new Map();
}
// Generate threshold signature shares
generateSignatureShares(message, privateKeyShares) {
const messageHash = crypto.createHash('sha256').update(message).digest('hex');
const signatures = [];
for (let i = 0; i < privateKeyShares.length; i++) {
const signature = this.curve.keyFromPrivate(privateKeyShares[i])
.sign(messageHash);
signatures.push({
index: i + 1,
signature: signature,
signer: this.curve.keyFromPrivate(privateKeyShares[i]).getPublic()
});
}
return signatures;
}
// Combine threshold signatures
combineSignatures(signatureShares, message) {
if (signatureShares.length < this.threshold) {
throw new Error(`Need at least ${this.threshold} signatures, got ${signatureShares.length}`);
}
// Use Lagrange interpolation to combine signatures
const combinedSignature = this.lagrangeInterpolation(
signatureShares.slice(0, this.threshold)
);
return combinedSignature;
}
lagrangeInterpolation(signatureShares) {
// Implement Lagrange interpolation for signature combination
// This is a simplified version - production implementation would be more complex
let combinedR = BigInt(0);
let combinedS = BigInt(0);
for (let i = 0; i < signatureShares.length; i++) {
let lagrangeCoeff = BigInt(1);
for (let j = 0; j < signatureShares.length; j++) {
if (i !== j) {
lagrangeCoeff *= BigInt(signatureShares[j].index) /
(BigInt(signatureShares[j].index) - BigInt(signatureShares[i].index));
}
}
combinedR += BigInt('0x' + signatureShares[i].signature.r.toString('hex')) * lagrangeCoeff;
combinedS += BigInt('0x' + signatureShares[i].signature.s.toString('hex')) * lagrangeCoeff;
}
return {
r: combinedR.toString(16),
s: combinedS.toString(16),
recoveryParam: signatureShares[0].signature.recoveryParam
};
}
}
For anonymous signatures where the signer is one of a group but identity is hidden:
// Ring signature implementation
class RingSignatureManager {
constructor() {
this.curve = new elliptic.ec('secp256k1');
}
// Create ring signature
createRingSignature(message, signerPrivateKey, publicKeys) {
const messageHash = crypto.createHash('sha256').update(message).digest('hex');
const ringSize = publicKeys.length;
// Find signer's position in ring
const signerPublicKey = this.curve.keyFromPrivate(signerPrivateKey).getPublic();
const signerIndex = publicKeys.findIndex(pk =>
pk.encode('hex') === signerPublicKey.encode('hex')
);
if (signerIndex === -1) {
throw new Error('Signer not found in ring');
}
// Generate random values for ring
const alpha = crypto.randomBytes(32);
const c = new Array(ringSize);
const s = new Array(ringSize);
// Initialize hash with message
let hashInput = messageHash;
// Generate random values for all positions except signer
for (let i = 0; i < ringSize; i++) {
if (i !== signerIndex) {
c[i] = crypto.randomBytes(32);
s[i] = crypto.randomBytes(32);
// Calculate ring component
const component = this.curve.g.mul(BigInt('0x' + s[i].toString('hex')))
.add(publicKeys[i].mul(BigInt('0x' + c[i].toString('hex'))));
hashInput += component.encode('hex');
}
}
// Calculate challenge for signer position
const totalHash = crypto.createHash('sha256').update(hashInput).digest();
c[signerIndex] = totalHash;
// Calculate signer's response
const privateKeyBN = BigInt('0x' + signerPrivateKey.toString('hex'));
const challengeBN = BigInt('0x' + c[signerIndex].toString('hex'));
const alphaBN = BigInt('0x' + alpha.toString('hex'));
s[signerIndex] = (alphaBN - privateKeyBN * challengeBN) % this.curve.n;
return {
c: c.map(val => val.toString('hex')),
s: s.map(val => val.toString('hex')),
ringPublicKeys: publicKeys.map(pk => pk.encode('hex')),
messageHash: messageHash
};
}
// Verify ring signature
verifyRingSignature(ringSignature, message) {
const messageHash = crypto.createHash('sha256').update(message).digest('hex');
if (messageHash !== ringSignature.messageHash) {
return false;
}
const publicKeys = ringSignature.ringPublicKeys.map(pk =>
this.curve.keyFromPublic(pk, 'hex').getPublic()
);
let hashInput = messageHash;
// Verify each ring component
for (let i = 0; i < publicKeys.length; i++) {
const component = this.curve.g.mul(BigInt('0x' + ringSignature.s[i]))
.add(publicKeys[i].mul(BigInt('0x' + ringSignature.c[i])));
hashInput += component.encode('hex');
}
const verificationHash = crypto.createHash('sha256').update(hashInput).digest('hex');
// Check if hash chain is valid
return verificationHash === ringSignature.c[0];
}
}
// Comprehensive signature debugging
function debugSignatureFailure(message, signature, expectedAddress) {
console.log('=== Signature Debug Information ===');
console.log('Message:', message);
console.log('Expected Address:', expectedAddress);
console.log('Signature:', signature);
try {
// Check signature format
if (signature.length !== 132) { // 0x + 130 hex chars
console.error('❌ Invalid signature length:', signature.length);
return false;
}
// Extract signature components
const r = signature.slice(0, 66);
const s = '0x' + signature.slice(66, 130);
const v = parseInt(signature.slice(130, 132), 16);
console.log('Signature components:');
console.log(' r:', r);
console.log(' s:', s);
console.log(' v:', v);
// Check v value validity
if (v < 27 || v > 28) {
console.error('❌ Invalid v value:', v);
return false;
}
// Try different message formats
const formats = [
message, // Raw message
`\x19Ethereum Signed Message:\n${message.length}${message}`, // Standard format
web3.utils.keccak256(message), // Pre-hashed
web3.utils.utf8ToHex(message) // Hex encoded
];
for (let i = 0; i < formats.length; i++) {
try {
const recovered = web3.eth.accounts.recover(formats[i], signature);
console.log(`Format ${i} recovery result:`, recovered);
if (recovered.toLowerCase() === expectedAddress.toLowerCase()) {
console.log('✅ Signature valid with format', i);
return true;
}
} catch (error) {
console.log(`Format ${i} failed:`, error.message);
}
}
console.error('❌ All recovery attempts failed');
return false;
} catch (error) {
console.error('❌ Debug failed:', error);
return false;
}
}
// Handle MetaMask-specific signing problems
async function handleMetaMaskSigning(message, address) {
try {
// Check MetaMask availability
if (!window.ethereum || !window.ethereum.isMetaMask) {
throw new Error('MetaMask not available');
}
// Check account connection
const accounts = await window.ethereum.request({
method: 'eth_accounts'
});
if (accounts.length === 0) {
throw new Error('No accounts connected');
}
if (accounts[0].toLowerCase() !== address.toLowerCase()) {
throw new Error('Wrong account selected in MetaMask');
}
// Try different signing methods
const signingMethods = [
{
method: 'personal_sign',
params: [message, address]
},
{
method: 'eth_sign',
params: [address, web3.utils.utf8ToHex(message)]
}
];
for (const method of signingMethods) {
try {
console.log(`Trying ${method.method}...`);
const signature = await window.ethereum.request({
method: method.method,
params: method.params
});
console.log(`✅ ${method.method} successful`);
return signature;
} catch (error) {
console.log(`❌ ${method.method} failed:`, error.message);
// Handle specific MetaMask errors
if (error.code === 4001) {
throw new Error('User rejected the request');
} else if (error.code === -32603) {
console.log('Internal JSON-RPC error, trying next method...');
}
}
}
throw new Error('All signing methods failed');
} catch (error) {
console.error('MetaMask signing failed:', error);
throw error;
}
}
// Handle network-specific signature differences
class NetworkSignatureHandler {
constructor(web3, networkId) {
this.web3 = web3;
this.networkId = networkId;
this.networkConfigs = {
1329: { // SEI Mainnet
name: 'SEI',
messagePrefix: '\x19Ethereum Signed Message:\n',
gasPrice: 'fast'
},
1: { // Ethereum Mainnet
name: 'Ethereum',
messagePrefix: '\x19Ethereum Signed Message:\n',
gasPrice: 'standard'
}
};
}
async signForNetwork(message, address) {
const config = this.networkConfigs[this.networkId];
if (!config) {
throw new Error(`Unsupported network: ${this.networkId}`);
}
console.log(`Signing for ${config.name} network...`);
// Network-specific message formatting
let formattedMessage = message;
if (config.messagePrefix) {
formattedMessage = config.messagePrefix + message.length + message;
}
try {
const signature = await this.web3.eth.personal.sign(formattedMessage, address);
// Verify signature works on this network
const recovered = this.web3.eth.accounts.recover(formattedMessage, signature);
if (recovered.toLowerCase() !== address.toLowerCase()) {
throw new Error('Signature verification failed for network');
}
console.log(`✅ Signature valid for ${config.name}`);
return signature;
} catch (error) {
console.error(`Network signing failed for ${config.name}:`, error);
throw error;
}
}
async verifyNetworkSignature(message, signature, address) {
const config = this.networkConfigs[this.networkId];
// Try both raw and prefixed message formats
const formats = [
message,
config.messagePrefix + message.length + message
];
for (const format of formats) {
try {
const recovered = this.web3.eth.accounts.recover(format, signature);
if (recovered.toLowerCase() === address.toLowerCase()) {
return true;
}
} catch (error) {
continue;
}
}
return false;
}
}
// Optimize multiple signature verifications
class BatchSignatureVerifier {
constructor(web3) {
this.web3 = web3;
this.verificationCache = new Map();
}
async batchVerifySignatures(verificationRequests) {
const results = [];
const uncachedRequests = [];
// Check cache first
for (const request of verificationRequests) {
const cacheKey = this.getCacheKey(request);
if (this.verificationCache.has(cacheKey)) {
results.push({
...request,
result: this.verificationCache.get(cacheKey),
fromCache: true
});
} else {
uncachedRequests.push(request);
}
}
// Verify uncached requests in parallel
const uncachedPromises = uncachedRequests.map(request =>
this.verifySingleSignature(request)
);
const uncachedResults = await Promise.all(uncachedPromises);
// Cache results and combine
for (let i = 0; i < uncachedResults.length; i++) {
const request = uncachedRequests[i];
const result = uncachedResults[i];
const cacheKey = this.getCacheKey(request);
this.verificationCache.set(cacheKey, result.isValid);
results.push({ ...request, ...result, fromCache: false });
}
return results;
}
getCacheKey(request) {
return crypto.createHash('sha256')
.update(request.message + request.signature + request.address)
.digest('hex');
}
async verifySingleSignature(request) {
try {
const startTime = performance.now();
const recovered = this.web3.eth.accounts.recover(
request.message,
request.signature
);
const isValid = recovered.toLowerCase() === request.address.toLowerCase();
const duration = performance.now() - startTime;
return {
isValid,
recoveredAddress: recovered,
verificationTime: duration
};
} catch (error) {
return {
isValid: false,
error: error.message,
verificationTime: 0
};
}
}
}
// Robust signature operation with retry logic
class RobustSignatureManager {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}
async signWithRetry(message, address) {
let lastError;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
console.log(`Signature attempt ${attempt}/${this.maxRetries}`);
const signature = await this.attemptSignature(message, address);
// Verify signature before returning
const isValid = await this.verifySignature(message, signature, address);
if (!isValid) {
throw new Error('Signature verification failed');
}
console.log(`✅ Signature successful on attempt ${attempt}`);
return signature;
} catch (error) {
console.error(`❌ Attempt ${attempt} failed:`, error.message);
lastError = error;
// Don't retry on user rejection
if (error.code === 4001) {
throw error;
}
// Wait before retry (exponential backoff)
if (attempt < this.maxRetries) {
const delay = this.baseDelay * Math.pow(2, attempt - 1);
console.log(`Waiting ${delay}ms before retry...`);
await this.sleep(delay);
}
}
}
throw new Error(`All ${this.maxRetries} signature attempts failed. Last error: ${lastError.message}`);
}
async attemptSignature(message, address) {
// Try different signing approaches
const approaches = [
() => this.signWithPersonalSign(message, address),
() => this.signWithEthSign(message, address),
() => this.signWithTypedData(message, address)
];
let lastError;
for (const approach of approaches) {
try {
return await approach();
} catch (error) {
lastError = error;
continue;
}
}
throw lastError;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
If you encounter issues with cryptographic signatures:
If you discover security vulnerabilities related to our signature implementation, please report them immediately to security@contracting.app with the subject line "SECURITY: Signature Vulnerability".
This guide covers the essential aspects of cryptographic signatures in our platform. For the latest updates and additional technical details, visit contracting.app/resources/signatures.