Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 62 additions & 9 deletions src/app/(sidebar)/transaction/dashboard/components/Signatures.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { StrKey, TransactionBuilder } from "@stellar/stellar-sdk";
import {
hash,
Keypair,
StrKey,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import { Icon, Link, Text } from "@stellar/design-system";

import { useStore } from "@/store/useStore";
Expand Down Expand Up @@ -103,7 +109,14 @@ export const Signatures = ({
const renderAuthEntriesTableBody = () => {
return authEntries.map((entry, index) => {
const rowKey = `auth-entry-row-${index}`;
const isMatch = verifyAuthEntryPublicKey(entry.publicKey, entry.address);
const isMatch =
isXdrInit &&
verifyAuthEntrySignature(
entry.rawEntry,
entry.publicKey,
entry.signature,
network.passphrase,
Comment on lines +112 to +118
);

return (
<tr role="row" key={rowKey}>
Expand Down Expand Up @@ -244,17 +257,55 @@ const renderSigner = (isVerified: boolean, signer: string) => {
};

/**
* Verifies that the public key bytes in an auth entry signature match the
* credential address (G... key).
* Verifies a Soroban authorization entry signature cryptographically.
*
* Reconstructs the HashIdPreimage (network ID, nonce, invocation,
* signatureExpirationLedger), hashes it, and verifies the Ed25519 signature.
*
* @param rawEntry - The JSON-decoded SorobanAuthorizationEntry
* @param publicKeyHex - The hex-encoded public key from the signature map
* @param signatureHex - The hex-encoded signature bytes from the signature map
* @param networkPassphrase - The network passphrase (e.g. "Test SDF Network ; September 2015")
* @returns true if the signature is cryptographically valid
*
* @example
* const isValid = verifyAuthEntrySignature(entry.rawEntry, entry.publicKey, entry.signature, network.passphrase);
*/
const verifyAuthEntryPublicKey = (
const verifyAuthEntrySignature = (
rawEntry: any,
publicKeyHex: string,
address: string,
signatureHex: string,
networkPassphrase: string,
): boolean => {
try {
const publicKeyBytes = Buffer.from(publicKeyHex, "hex");
const derivedAddress = StrKey.encodeEd25519PublicKey(publicKeyBytes);
return derivedAddress === address;
const entryXdrBase64 = StellarXdr.encode(
"SorobanAuthorizationEntry",
JSON.stringify(rawEntry),
);
const authEntry = xdr.SorobanAuthorizationEntry.fromXDR(
entryXdrBase64,
"base64",
);

const addrAuth = authEntry.credentials().address();
const networkId = hash(Buffer.from(networkPassphrase));

const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
new xdr.HashIdPreimageSorobanAuthorization({
networkId,
nonce: addrAuth.nonce(),
invocation: authEntry.rootInvocation(),
signatureExpirationLedger: addrAuth.signatureExpirationLedger(),
}),
);

const payload = hash(preimage.toXDR());
const stellarAddress = StrKey.encodeEd25519PublicKey(
Buffer.from(publicKeyHex, "hex"),
);
const keypair = Keypair.fromPublicKey(stellarAddress);

return keypair.verify(payload, Buffer.from(signatureHex, "hex"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify the signer is authorized for the auth address

For address auth entries where the signature map contains a public_key that is not actually authorized by entry.address, this return value can still be true: the preimage constructed above is only network/nonce/expiration/invocation and does not bind addrAuth.address(), and the derived key is never checked against the credential address or its signer set. A forged or failed auth entry can therefore display a green check next to a victim address simply by including an attacker-controlled key and a valid signature from that key over the same payload.

Useful? React with 👍 / 👎.

} catch {
return false;
}
Expand All @@ -266,6 +317,7 @@ type AuthEntryInfo = {
publicKey: string;
signature: string;
contractId: string;
rawEntry: any;
};

const getAuthEntries = (operations: any[] | undefined): AuthEntryInfo[] => {
Expand Down Expand Up @@ -305,6 +357,7 @@ const parseAuthEntry = (authEntry: any): AuthEntryInfo | null => {
publicKey,
signature,
contractId: contractFn ? contractFn.contract_address : "-",
rawEntry: authEntry,
};
};

Expand Down
Loading
Loading