steem-js : appels signés et vérification de signature
Certaines données et opérations exigent de prouver la propriété d'un compte. steem-js est livré avec deux boîtes à outils complémentaires : steem.api.signedCall pour les requêtes JSON-RPC authentifiées, et signatureVerification pour contrôler les signatures côté récepteur. Cette page fusionne les guides d'exemples de la bibliothèque consacrés aux appels signés et à la vérification de signature ; les fonctions auxiliaires de lecture qu'ils utilisent sont documentées dans Lire les données de la chaîne.
Appels RPC signés
Appels RPC signés
Pour les appels RPC sécurisés nécessitant une authentification, utilisez signedCall. Cette méthode signe la requête avec votre clé privée afin de prouver la propriété du compte.
signedCall
Effectue un appel JSON-RPC signé vers la blockchain Steem. La requête est signée cryptographiquement pour authentifier l'appelant. Il s'agit de la version basée sur les rappels (callbacks). Pour une utilisation basée sur les Promise, voir signedCallAsync ci-dessous.
steem.api.signedCall(method, params, account, privateKey, callback);Description des paramètres :
| Paramètre | Type de données | Description |
|---|---|---|
| method | string | Nom complet de la méthode RPC (ex. condenser_api.get_account_history) |
| params | array | Paramètres de la méthode RPC |
| account | string | Nom du compte émettant la requête |
| privateKey | string | Clé privée (au format WIF) servant à signer |
| callback | function | Fonction de rappel callback(err, result) |
Prérequis :
- Utilise le transport HTTP pour tous les appels API
- Nécessite une clé privée valide au format WIF
- Le compte doit correspondre à la clé privée
Exemple d'appel :
// Configure for HTTP transport
steem.config.set({
nodes: ['https://api.steemit.com'],
transport: 'http'
});
// Make a signed call
const privateKey = '5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8';
const account = 'username';
// Using callback style
steem.api.signedCall(
'condenser_api.get_account_history',
[account, -1, 10],
account,
privateKey,
function(err, result) {
if (err) {
console.error('Signed call failed:', err);
} else {
console.log('Account history:', result);
}
}
);signedCallAsync
Version de signedCall basée sur les Promise, à utiliser avec la syntaxe async/await.
steem.api.signedCallAsync(method, params, account, privateKey);Valeur de retour : une Promise qui se résout avec le résultat ou est rejetée avec une erreur.
Exemple avec Promise :
// Using signedCallAsync with async/await
try {
const result = await steem.api.signedCallAsync(
'condenser_api.get_account_history',
['username', -1, 10],
'username',
privateKey
);
console.log('Result:', result);
} catch (error) {
console.error('Error:', error);
}Notes de sécurité :
- La requête inclut un horodatage et un nonce pour prévenir les attaques par rejeu (replay attack)
- Les signatures expirent après 60 secondes
- Les clés privées ne sont jamais transmises, seule la signature l'est
- Chaque requête est signée de manière unique avec une preuve cryptographique
Cas d'usage :
- Accéder à des données privées d'un compte
- Effectuer des appels API authentifiés
- Prouver la propriété d'un compte
- Communiquer de façon sécurisée avec les nœuds Steem
Vérification de signature
Pour vérifier les requêtes signées et les messages, Steem.js fournit des utilitaires complets :
import { signatureVerification } from '@steemit/steem-js/api';
// Verify a signed RPC request
const getAccountKeys = signatureVerification.createApiVerificationFunction(steem.api);
const result = await signatureVerification.verifySignedRequest(signedRequest, getAccountKeys);
if (result.valid) {
console.log('✅ Signature verified for account:', result.account);
console.log('Decoded parameters:', result.params);
} else {
console.log('❌ Verification failed:', result.error);
}
// Verify a simple message signature
const message = 'Hello, Steem!';
const signature = steem.auth.sign(message, privateKey);
const publicKey = steem.auth.wifToPublic(privateKey);
const isValid = signatureVerification.verifyMessageSignature(message, signature, publicKey);Fonctionnalités de vérification :
- Validation des requêtes signées (Signed Request Validation) : vérifier des requêtes RPC signées complètes
- Vérification de signature de message (Message Signature Verification) : vérifier des signatures de messages individuelles
- Vérification par lot (Batch Verification) : vérifier plusieurs requêtes simultanément
- Contrôle d'expiration (Expiration Checking) : validation automatique de l'expiration des signatures
- Validation de format (Format Validation) : valider les formats de signature et de clé
- Extraction des clés de compte (Account Key Extraction) : extraire les clés publiques des données du compte
Le reste de cette page rassemble des exemples complets de signedCall et de vérification de signature.
Exemples de signedCall
Les sous-sections ci-dessous reprennent les exemples complets de signedCall issus de la documentation de la bibliothèque.
Ce document fournit des exemples complets d'utilisation de signedCall dans Steem.js pour des requêtes API authentifiées.
Aperçu
signedCall sert à effectuer des appels JSON-RPC authentifiés vers la blockchain Steem. Il signe cryptographiquement les requêtes pour prouver la propriété du compte et accéder aux données privées ou restreintes.
Passez le nom complet de la méthode RPC (ex. condenser_api.get_account_history). Les anciennes méthodes de lecture utilisent l'espace de noms condenser_api sur les nœuds actuels ; voir le routage API dans le tutoriel de démarrage.
Configuration de base
import { steem } from '@steemit/steem-js';
// Configure for HTTP transport (required for signedCall)
steem.config.set({
nodes: ['https://api.steemit.com'],
transport: 'http'
});Méthodes d'authentification
Avec nom d'utilisateur et mot de passe
// Generate private key from username/password
const username = 'your-username';
const password = 'your-password';
const activeKey = steem.auth.toWif(username, password, 'active');
const postingKey = steem.auth.toWif(username, password, 'posting');Avec une clé privée existante
// Use existing WIF private key
const privateKey = '5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8';
const account = 'username';Exemples signedCall de base
1. Obtenir l'historique d'un compte (avec authentification)
function getAccountHistorySigned(account, privateKey) {
return new Promise((resolve, reject) => {
steem.api.signedCall(
'condenser_api.get_account_history',
[account, -1, 100],
account,
privateKey,
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
}
// Usage
const account = 'username';
const privateKey = steem.auth.toWif(account, 'password', 'active');
getAccountHistorySigned(account, privateKey)
.then(history => {
console.log('Account history:', history);
})
.catch(error => {
console.error('Error:', error);
});2. Obtenir des informations privées d'un compte
async function getPrivateAccountInfo(account, privateKey) {
try {
// Get detailed account information with private data
const result = await new Promise((resolve, reject) => {
steem.api.signedCall(
'condenser_api.get_accounts',
[[account]],
account,
privateKey,
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
return result[0]; // Return first account
} catch (error) {
throw new Error(`Failed to get account info: ${error.message}`);
}
}
// Usage
getPrivateAccountInfo('username', privateKey)
.then(accountInfo => {
console.log('Private account data:', {
name: accountInfo.name,
balance: accountInfo.balance,
sbd_balance: accountInfo.sbd_balance,
vesting_shares: accountInfo.vesting_shares
});
});3. Accéder aux informations d'un witness (témoin), avec authentification
function getWitnessInfoSigned(witnessAccount, privateKey) {
return new Promise((resolve, reject) => {
steem.api.signedCall(
'condenser_api.get_witness_by_account',
[witnessAccount],
witnessAccount,
privateKey,
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
}
// Usage
getWitnessInfoSigned('witness-account', privateKey)
.then(witnessInfo => {
console.log('Witness information:', witnessInfo);
});Exemples avancés
4. Requêtes signées par lot
class SignedApiClient {
constructor(account, privateKey) {
this.account = account;
this.privateKey = privateKey;
}
async call(method, params) {
return new Promise((resolve, reject) => {
steem.api.signedCall(
method,
params,
this.account,
this.privateKey,
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
}
async getAccountHistory(limit = 100) {
return this.call('condenser_api.get_account_history', [this.account, -1, limit]);
}
async getFollowers(limit = 100) {
return this.call('condenser_api.get_followers', [this.account, '', 'blog', limit]);
}
async getFollowing(limit = 100) {
return this.call('condenser_api.get_following', [this.account, '', 'blog', limit]);
}
}
// Usage
const client = new SignedApiClient('username', privateKey);
async function getUserData() {
try {
const [history, followers, following] = await Promise.all([
client.getAccountHistory(50),
client.getFollowers(100),
client.getFollowing(100)
]);
console.log('User data loaded:', {
historyCount: history.length,
followersCount: followers.length,
followingCount: following.length
});
} catch (error) {
console.error('Failed to load user data:', error);
}
}
getUserData();5. Gestion des erreurs et logique de réessai
class RobustSignedClient {
constructor(account, privateKey, maxRetries = 3) {
this.account = account;
this.privateKey = privateKey;
this.maxRetries = maxRetries;
}
async callWithRetry(method, params, retryCount = 0) {
try {
return await this.makeSignedCall(method, params);
} catch (error) {
if (retryCount < this.maxRetries && this.isRetryableError(error)) {
console.log(`Retrying ${method}, attempt ${retryCount + 1}/${this.maxRetries}`);
await this.delay(1000 * (retryCount + 1)); // Exponential backoff
return this.callWithRetry(method, params, retryCount + 1);
}
throw error;
}
}
makeSignedCall(method, params) {
return new Promise((resolve, reject) => {
steem.api.signedCall(method, params, this.account, this.privateKey, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
isRetryableError(error) {
const retryableErrors = [
'network error',
'timeout',
'connection refused',
'signature expired'
];
return retryableErrors.some(errorType =>
error.message.toLowerCase().includes(errorType)
);
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const robustClient = new RobustSignedClient('username', privateKey);
robustClient.callWithRetry('condenser_api.get_account_history', ['username', -1, 100])
.then(result => console.log('Success:', result))
.catch(error => console.error('Final error:', error));6. Validation et sécurité
class SecureSignedClient {
constructor(account, privateKey) {
this.account = account;
this.privateKey = privateKey;
this.validateCredentials();
}
validateCredentials() {
// Validate account name
if (!this.account || typeof this.account !== 'string') {
throw new Error('Invalid account name');
}
// Validate private key format
if (!steem.auth.isWif(this.privateKey)) {
throw new Error('Invalid private key format');
}
// Verify key matches account (optional, requires additional API call)
const publicKey = steem.auth.wifToPublic(this.privateKey);
console.log(`Using public key: ${publicKey} for account: ${this.account}`);
}
async secureCall(method, params) {
// Additional security checks
// Prefer condenser_api.* for legacy reads; database_api.* for config/validation and list/find APIs.
if (!method.startsWith('condenser_api.') && !method.startsWith('database_api.')) {
throw new Error('Only condenser_api and database_api methods are allowed');
}
// Rate limiting (simple implementation)
if (this.lastCallTime && Date.now() - this.lastCallTime < 1000) {
throw new Error('Rate limit: Wait at least 1 second between calls');
}
this.lastCallTime = Date.now();
return new Promise((resolve, reject) => {
steem.api.signedCall(method, params, this.account, this.privateKey, (err, result) => {
if (err) {
console.error(`Signed call failed for ${method}:`, err.message);
reject(err);
} else {
console.log(`Signed call successful for ${method}`);
resolve(result);
}
});
});
}
}
// Usage
const secureClient = new SecureSignedClient('username', privateKey);
secureClient.secureCall('condenser_api.get_accounts', [['username']])
.then(result => console.log('Secure result:', result))
.catch(error => console.error('Secure error:', error));Cas d'usage courants
Gestion de compte
// Get comprehensive account data
async function getFullAccountData(account, privateKey) {
const client = new SignedApiClient(account, privateKey);
const [
accountInfo,
history,
followers,
following
] = await Promise.all([
client.call('condenser_api.get_accounts', [[account]]),
client.call('condenser_api.get_account_history', [account, -1, 100]),
client.call('condenser_api.get_followers', [account, '', 'blog', 1000]),
client.call('condenser_api.get_following', [account, '', 'blog', 1000])
]);
return {
account: accountInfo[0],
recentActivity: history,
followers: followers,
following: following
};
}Analyse de contenu
// Analyze user's content and voting patterns
async function analyzeUserActivity(account, privateKey) {
const client = new SignedApiClient(account, privateKey);
const history = await client.call('condenser_api.get_account_history', [account, -1, 1000]);
const analysis = {
posts: 0,
comments: 0,
votes: 0,
transfers: 0,
other: 0
};
history.forEach(([, operation]) => {
const opType = operation.op[0];
switch (opType) {
case 'comment':
if (operation.op[1].parent_author === '') {
analysis.posts++;
} else {
analysis.comments++;
}
break;
case 'vote':
analysis.votes++;
break;
case 'transfer':
analysis.transfers++;
break;
default:
analysis.other++;
}
});
return analysis;
}Gestion des erreurs
Erreurs fréquentes et solutions
function handleSignedCallError(error) {
switch (true) {
case error.message.includes('RPC methods can only be called when using http transport'):
console.error('Solution: Configure HTTP transport instead of WebSocket');
console.log('steem.config.set({ transport: "http" });');
break;
case error.message.includes('Signature expired'):
console.error('Solution: Request expired, retry with fresh signature');
break;
case error.message.includes('Invalid private key'):
console.error('Solution: Check private key format (should be WIF)');
break;
case error.message.includes('Account not found'):
console.error('Solution: Verify account name is correct');
break;
default:
console.error('Unknown error:', error.message);
}
}
// Usage in error handling
steem.api.signedCall(method, params, account, privateKey, (err, result) => {
if (err) {
handleSignedCallError(err);
return;
}
console.log('Success:', result);
});Utiliser signedCallAsync (basé sur les Promise)
Dans du code async/await moderne, utilisez signedCallAsync :
try {
const result = await steem.api.signedCallAsync(
'conveyor.is_email_registered',
[{ email: 'user@example.com' }],
'your-account',
'your-private-key-wif'
);
console.log('Success:', result);
} catch (error) {
console.error('Error:', error);
}Cet appel est équivalent à la version par rappels, mais utilise les Promise, ce qui le rend plus simple à employer avec la syntaxe async/await.
Bonnes pratiques
- Utilisez toujours des points de terminaison HTTPS pour les appels signés
- Validez les clés privées avant tout appel
- Implémentez une logique de réessai pour les erreurs réseau
- Utilisez une limitation de débit pour ne pas saturer l'API
- Gérez élégamment l'expiration des signatures
- Ne journalisez jamais les clés privées en production
- Utilisez des variables d'environnement pour les données sensibles
- Implémentez une gestion d'erreurs appropriée pour tous les scénarios
Considérations de sécurité pour signedCall
- Les clés privées ne sont jamais transmises sur le réseau
- Chaque requête inclut un nonce et un horodatage uniques
- Les signatures expirent après 60 secondes pour prévenir les attaques par rejeu
- Vérifiez toujours que le point de terminaison utilise HTTPS
- Conservez les clés privées en sécurité (jamais en clair)
- Envisagez des portefeuilles matériels pour les applications de production
Tests
// Test signedCall functionality
describe('SignedCall Tests', () => {
const testAccount = 'test-account';
const testPrivateKey = '5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8';
it('should make successful signed call', (done) => {
steem.api.signedCall(
'condenser_api.get_accounts',
[[testAccount]],
testAccount,
testPrivateKey,
(err, result) => {
expect(err).toBeNull();
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
done();
}
);
});
it('should handle invalid private key', (done) => {
steem.api.signedCall(
'condenser_api.get_accounts',
[[testAccount]],
testAccount,
'invalid-key',
(err, result) => {
expect(err).toBeDefined();
expect(err.message).toContain('Invalid private key');
done();
}
);
});
});Ce guide complet devrait vous aider à mettre en œuvre signedCall efficacement dans vos applications Steem.js.
Exemples de vérification de signature
Ce document fournit des exemples complets de vérification de signatures dans Steem.js, notamment les requêtes RPC signées et les signatures de messages en général.
Aperçu de la vérification
La vérification de signature est cruciale pour :
- Valider l'authenticité des requêtes RPC signées
- Garantir que les messages n'ont pas été altérés
- Vérifier la propriété d'un compte
- Mettre en place des systèmes d'authentification sécurisés
Configuration de base
import { steem } from '@steemit/steem-js';
import { signatureVerification } from '@steemit/steem-js/api';
// Configure API
steem.config.set({
nodes: ['https://api.steemit.com'],
transport: 'http'
});1. Vérifier les requêtes RPC signées
Vérification de base
async function verifySignedRpcRequest(signedRequest) {
try {
// Create account key fetcher
const getAccountKeys = signatureVerification.createApiVerificationFunction(steem.api);
// Verify the signed request
const result = await signatureVerification.verifySignedRequest(signedRequest, getAccountKeys);
if (result.valid) {
console.log('✅ Signature verified successfully');
console.log('Account:', result.account);
console.log('Timestamp:', result.timestamp);
console.log('Decoded params:', result.params);
return true;
} else {
console.log('❌ Signature verification failed:', result.error);
return false;
}
} catch (error) {
console.error('Verification error:', error);
return false;
}
}
// Example usage
const signedRequest = {
jsonrpc: '2.0',
method: 'condenser_api.get_accounts',
id: 1,
params: {
__signed: {
account: 'username',
nonce: '1234567890abcdef',
params: 'WyJ1c2VybmFtZSJd', // base64 encoded ["username"]
signatures: ['304502210...'], // hex signature
timestamp: '2025-01-01T12:00:00.000Z'
}
}
};
verifySignedRpcRequest(signedRequest);Cycle complet signature + vérification
async function completeSignAndVerifyWorkflow() {
const account = 'username';
const password = 'password';
const privateKey = steem.auth.toWif(account, password, 'active');
try {
// Step 1: Create and sign a request
const request = {
method: 'condenser_api.get_accounts',
params: [[account]],
id: 1
};
const signedRequest = steem.api.signRequest(request, account, [privateKey]);
console.log('✅ Request signed successfully');
// Step 2: Verify the signed request
const getAccountKeys = signatureVerification.createApiVerificationFunction(steem.api);
const verificationResult = await signatureVerification.verifySignedRequest(
signedRequest,
getAccountKeys
);
if (verificationResult.valid) {
console.log('✅ Signature verification successful');
console.log('Verified account:', verificationResult.account);
console.log('Original params:', verificationResult.params);
} else {
console.log('❌ Verification failed:', verificationResult.error);
}
return verificationResult;
} catch (error) {
console.error('Workflow error:', error);
return { valid: false, error: error.message };
}
}
completeSignAndVerifyWorkflow();2. Vérification de signature de message
Vérification simple d'un message
function verifySimpleMessage() {
const message = 'Hello, Steem blockchain!';
const privateKey = '5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8';
const publicKey = steem.auth.wifToPublic(privateKey);
// Sign the message
const signature = steem.auth.sign(message, privateKey);
console.log('Message signed:', signature);
// Verify the signature
const isValid = signatureVerification.verifyMessageSignature(message, signature, publicKey);
console.log('Signature valid:', isValid);
return isValid;
}
verifySimpleMessage();Vérification de plusieurs signatures
function verifyMultipleSignatures() {
const message = 'Multi-signature test message';
// Create multiple key pairs
const keys = [
{ private: '5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8' },
{ private: '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3' }
].map(k => ({
...k,
public: steem.auth.wifToPublic(k.private)
}));
// Sign with multiple keys
const signatures = keys.map(key => steem.auth.sign(message, key.private));
const publicKeys = keys.map(key => key.public);
console.log('Signatures created:', signatures.length);
// Verify multiple signatures
const result = signatureVerification.verifyMultipleSignatures(
message,
signatures,
publicKeys
);
console.log('Verification result:', {
verified: result.verified,
validSignatures: result.validSignatures,
totalChecked: result.details.length
});
// Show detailed results
result.details.forEach((detail, index) => {
console.log(`Signature ${index + 1}:`, detail.valid ? '✅' : '❌');
});
return result;
}
verifyMultipleSignatures();3. Vérification par lot
Vérifier plusieurs requêtes signées
async function batchVerifyRequests() {
const account = 'username';
const privateKey = steem.auth.toWif(account, 'password', 'active');
// Create multiple signed requests
const requests = [
{ method: 'condenser_api.get_accounts', params: [[account]], id: 1 },
{ method: 'condenser_api.get_account_history', params: [account, -1, 10], id: 2 },
{ method: 'condenser_api.get_followers', params: [account, '', 'blog', 10], id: 3 }
];
const signedRequests = requests.map(req =>
steem.api.signRequest(req, account, [privateKey])
);
console.log(`Created ${signedRequests.length} signed requests`);
// Batch verify all requests
const getAccountKeys = signatureVerification.createApiVerificationFunction(steem.api);
const results = await signatureVerification.batchVerifySignedRequests(
signedRequests,
getAccountKeys
);
console.log('Batch verification results:');
results.forEach((result, index) => {
console.log(`Request ${index + 1}:`, result.valid ? '✅' : '❌', result.error || 'Valid');
});
const validCount = results.filter(r => r.valid).length;
console.log(`${validCount}/${results.length} requests verified successfully`);
return results;
}
batchVerifyRequests();4. Scénarios de vérification avancés
Fournisseur de clés de compte personnalisé
class CustomAccountKeyProvider {
constructor() {
this.keyCache = new Map();
}
async getAccountKeys(account) {
// Check cache first
if (this.keyCache.has(account)) {
console.log(`Using cached keys for ${account}`);
return this.keyCache.get(account);
}
try {
// Fetch from API
const accountData = await new Promise((resolve, reject) => {
steem.api.call('condenser_api.get_accounts', [[account]], (err, result) => {
if (err) reject(err);
else resolve(result[0]);
});
});
const keys = signatureVerification.extractAccountKeys(accountData);
// Cache the keys
this.keyCache.set(account, keys);
console.log(`Fetched and cached keys for ${account}`);
return keys;
} catch (error) {
throw new Error(`Failed to get keys for ${account}: ${error.message}`);
}
}
}
async function useCustomKeyProvider() {
const keyProvider = new CustomAccountKeyProvider();
const account = 'username';
// Create a signed request
const request = steem.api.signRequest(
{ method: 'condenser_api.get_accounts', params: [[account]], id: 1 },
account,
[steem.auth.toWif(account, 'password', 'active')]
);
// Verify using custom provider
const result = await signatureVerification.verifySignedRequest(
request,
(acc) => keyProvider.getAccountKeys(acc)
);
console.log('Custom verification result:', result.valid ? '✅' : '❌');
return result;
}
useCustomKeyProvider();Contrôle de l'expiration des signatures
function checkSignatureExpiration() {
const timestamps = [
new Date().toISOString(), // Current time
new Date(Date.now() - 30000).toISOString(), // 30 seconds ago
new Date(Date.now() - 120000).toISOString(), // 2 minutes ago (expired)
'invalid-timestamp'
];
console.log('Checking signature expiration:');
timestamps.forEach((timestamp, index) => {
const isExpired = signatureVerification.isSignatureExpired(timestamp);
console.log(`Timestamp ${index + 1}: ${isExpired ? '❌ Expired' : '✅ Valid'}`);
console.log(` Value: ${timestamp}`);
});
// Custom expiration time (5 minutes)
const customExpiry = signatureVerification.isSignatureExpired(
new Date(Date.now() - 360000).toISOString(), // 6 minutes ago
300000 // 5 minutes in ms
);
console.log(`Custom expiry check: ${customExpiry ? '❌ Expired' : '✅ Valid'}`);
}
checkSignatureExpiration();Validation de format
function validateFormats() {
const testSignatures = [
'304502210...', // Valid hex signature
'invalid-signature', // Invalid format
'', // Empty
'123' // Too short
];
const testPublicKeys = [
'STM6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV', // Valid Steem public key
'invalid-public-key', // Invalid format
'', // Empty
'STM123' // Invalid checksum
];
console.log('Signature format validation:');
testSignatures.forEach((sig, index) => {
const isValid = signatureVerification.isValidSignatureFormat(sig);
console.log(`Signature ${index + 1}: ${isValid ? '✅' : '❌'} - ${sig.substring(0, 20)}...`);
});
console.log('\nPublic key format validation:');
testPublicKeys.forEach((key, index) => {
const isValid = signatureVerification.isValidPublicKeyFormat(key);
console.log(`Key ${index + 1}: ${isValid ? '✅' : '❌'} - ${key.substring(0, 20)}...`);
});
}
validateFormats();5. Cas d'usage réels
Passerelle API sécurisée
class SecureApiGateway {
constructor(api) {
this.api = api;
this.getAccountKeys = signatureVerification.createApiVerificationFunction(api);
}
async processSignedRequest(signedRequest) {
try {
// Step 1: Verify the signature
const verification = await signatureVerification.verifySignedRequest(
signedRequest,
this.getAccountKeys
);
if (!verification.valid) {
return {
success: false,
error: 'Invalid signature',
details: verification.error
};
}
// Step 2: Check if signature is not expired
if (signatureVerification.isSignatureExpired(verification.timestamp)) {
return {
success: false,
error: 'Signature expired'
};
}
// Step 3: Execute the original request
const result = await new Promise((resolve, reject) => {
this.api.call(signedRequest.method, verification.params, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
return {
success: true,
account: verification.account,
result: result
};
} catch (error) {
return {
success: false,
error: 'Processing failed',
details: error.message
};
}
}
}
// Usage
async function useSecureGateway() {
const gateway = new SecureApiGateway(steem.api);
// Create a signed request
const signedRequest = steem.api.signRequest(
{ method: 'condenser_api.get_accounts', params: [['username']], id: 1 },
'username',
[steem.auth.toWif('username', 'password', 'active')]
);
// Process through secure gateway
const result = await gateway.processSignedRequest(signedRequest);
if (result.success) {
console.log('✅ Secure request processed successfully');
console.log('Account:', result.account);
console.log('Result:', result.result);
} else {
console.log('❌ Secure request failed:', result.error);
}
return result;
}
useSecureGateway();Système d'authentification de messages
class MessageAuthSystem {
constructor() {
this.trustedKeys = new Set();
}
addTrustedKey(publicKey) {
if (signatureVerification.isValidPublicKeyFormat(publicKey)) {
this.trustedKeys.add(publicKey);
console.log(`✅ Added trusted key: ${publicKey.substring(0, 20)}...`);
} else {
console.log(`❌ Invalid key format: ${publicKey}`);
}
}
verifyMessage(message, signature, publicKey) {
// Check if key is trusted
if (!this.trustedKeys.has(publicKey)) {
return {
valid: false,
error: 'Untrusted public key'
};
}
// Verify signature
const isValid = signatureVerification.verifyMessageSignature(message, signature, publicKey);
return {
valid: isValid,
publicKey: publicKey,
message: message,
error: isValid ? null : 'Invalid signature'
};
}
batchVerifyMessages(messages) {
return messages.map(msg => this.verifyMessage(msg.message, msg.signature, msg.publicKey));
}
}
// Usage
function useMessageAuthSystem() {
const authSystem = new MessageAuthSystem();
// Add trusted keys
const trustedKey = steem.auth.wifToPublic('5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8');
authSystem.addTrustedKey(trustedKey);
// Create and verify messages
const message = 'Authenticated message';
const signature = steem.auth.sign(message, '5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8');
const result = authSystem.verifyMessage(message, signature, trustedKey);
console.log('Message authentication result:', result.valid ? '✅' : '❌');
if (!result.valid) {
console.log('Error:', result.error);
}
return result;
}
useMessageAuthSystem();6. Vérification de signature de transaction
steem.auth.verifyTransaction(signedTx, publicKey) vérifie que les signatures d'une transaction ont bien été produites par la clé publique indiquée. Contrairement à la vérification des messages/requêtes RPC ci-dessus, elle opère au niveau de la transaction : elle reconstruit le condensé exact sha256(chain_id ‖ serializeTransaction(trx)) que signTransaction signe, puis contrôle chaque signature par rapport à celui-ci.
Pourquoi c'est important : un relais ou un serveur de portefeuille peut prouver qu'une transaction soumise par un client a bien été signée par la clé du compte déclaré avant de la transmettre à la chaîne — comblant une lacune de défense en profondeur que la seule validation de la forme de la transaction ne peut couvrir.
Cycle de base signature-vérification
import { steem } from '@steemit/steem-js';
const wif = '5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8';
const publicKey = steem.auth.wifToPublic(wif);
const tx = {
ref_block_num: 123,
ref_block_prefix: 456789,
expiration: '2026-07-10T00:00:00',
operations: [['transfer', {
from: 'alice', to: 'bob', amount: '1.000 STEEM', memo: ''
}]],
extensions: [],
};
// Client signs the transaction locally
const signedTx = steem.auth.signTransaction(tx, [wif]);
// Server (or same process) verifies it against the public key
const isValid = steem.auth.verifyTransaction(signedTx, publicKey);
console.log(isValid ? '✅ Valid signature' : '❌ Invalid signature');
// => ✅ Valid signatureVérification côté serveur (service de relais)
C'est le cas d'usage de sécurité principal : un serveur de portefeuille ou de relais reçoit d'un client une transaction signée et doit confirmer de manière indépendante le signataire avant la diffusion (broadcast).
import { steem } from '@steemit/steem-js';
/**
* Verify a client-submitted transaction before relaying it to the chain.
*
* @param {object} signedTx - Transaction signed by the client (has .signatures)
* @param {string} publicKey - The STM... public key the server expects the
* account to sign with (e.g. fetched from get_accounts).
* @returns {boolean} true if the signature is cryptographically valid.
*/
function verifyClientTransaction(signedTx, publicKey) {
if (!signedTx || !Array.isArray(signedTx.signatures) || signedTx.signatures.length === 0) {
console.error('❌ Transaction has no signatures');
return false;
}
const isValid = steem.auth.verifyTransaction(signedTx, publicKey);
if (!isValid) {
console.error('❌ Signature does not match the claimed public key');
return false;
}
console.log('✅ Transaction signed by the claimed key — safe to relay');
return true;
}
// Usage in a relay route:
// const account = await steem.api.getAccountsAsync([username]);
// const expectedPubKey = account[0].active.key_auths[0][0];
// if (!verifyClientTransaction(clientSignedTx, expectedPubKey)) {
// return res.status(403).json({ error: 'Invalid signature' });
// }
// await steem.broadcast.sendAsync(clientSignedTx);Cas de rejet
// 1. Wrong public key
const wrongPub = steem.auth.wifToPublic('5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3');
console.log(steem.auth.verifyTransaction(signedTx, wrongPub)); // false
// 2. Tampered transaction (operation changed after signing)
const tampered = JSON.parse(JSON.stringify(signedTx));
tampered.operations[0][1].amount = '999.000 STEEM';
console.log(steem.auth.verifyTransaction(tampered, publicKey)); // false
// 3. Missing signatures entirely
const unsigned = { ...tx };
console.log(steem.auth.verifyTransaction(unsigned, publicKey)); // falseConstruire le condensé manuellement avec serializeTransaction
Pour des cas d'usage avancés (par exemple vérifier avec une pile cryptographique personnalisée), le sérialiseur binaire est exporté sous la forme steem.auth.serializeTransaction(trx) afin que vous puissiez reconstruire vous-même le condensé de signature :
import { steem } from '@steemit/steem-js';
import { sha256 } from '@noble/hashes/sha2';
// The digest signTransaction signs over:
const buf = steem.auth.serializeTransaction(tx); // Buffer (binary transaction)
const chainId = Buffer.alloc(32, 0); // mainnet chain_id (all-zero)
const digest = Buffer.from(sha256(Buffer.concat([chainId, buf])));
console.log('Signing digest:', digest.toString('hex')); // 32-byte hexRemarque :
serializeTransactionn'inclut pas le champsignaturesdans sa sortie lorsque l'entrée n'en contient pas — au même titre quesignTransaction, qui sérialise le corps de la transaction avant d'y attacher les signatures.
Erreurs de vérification courantes
function handleVerificationErrors() {
const commonErrors = [
{
name: 'Invalid signature format',
test: () => signatureVerification.isValidSignatureFormat('invalid')
},
{
name: 'Invalid public key format',
test: () => signatureVerification.isValidPublicKeyFormat('invalid')
},
{
name: 'Expired signature',
test: () => !signatureVerification.isSignatureExpired(
new Date(Date.now() - 120000).toISOString()
)
}
];
console.log('Common verification error checks:');
commonErrors.forEach(error => {
try {
const result = error.test();
console.log(`${error.name}: ${result ? '✅ Pass' : '❌ Fail'}`);
} catch (e) {
console.log(`${error.name}: ❌ Exception - ${e.message}`);
}
});
}
handleVerificationErrors();Bonnes pratiques de vérification
- Validez toujours les formats d'entrée avant la vérification
- Contrôlez l'expiration des signatures pour prévenir les attaques par rejeu
- Utilisez des sources de clés fiables pour la vérification
- Implémentez une gestion d'erreurs appropriée pour tous les scénarios
- Mettez en cache les clés de compte pour améliorer les performances
- Journalisez les tentatives de vérification pour l'audit de sécurité
- Utilisez la vérification par lot pour plusieurs requêtes
- Implémentez une limitation de débit pour prévenir les abus
Considérations de sécurité pour la vérification
- Par défaut, les signatures expirent après 60 secondes
- Vérifiez toujours par rapport aux clés publiques du bon compte
- Utilisez des points de terminaison HTTPS pour prévenir les attaques de l'homme du milieu
- Validez tous les paramètres d'entrée avant traitement
- Mettez en place une journalisation appropriée pour l'audit de sécurité
- Envisagez des couches d'authentification supplémentaires pour les opérations sensibles
Ce guide complet couvre tous les aspects de la vérification de signature dans Steem.js, de la vérification de base des messages aux systèmes d'authentification complexes.
Et ensuite ?
- Lire les données de la chaîne pour les fonctions auxiliaires de lecture utilisées dans les requêtes signées.
- Migration des tests d'authentification RPC pour voir comment ce mécanisme est mis à l'épreuve dans la suite de tests.