-
Notifications
You must be signed in to change notification settings - Fork 99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add signature verification to xdr viewer for transaction envelopes #348
Conversation
Fails for hash(x) |
Now passes for pre-auth tx and hash x signers, should be good for review @bartekn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for a late review. Overall the PR is good. We should leverage the existing code in stellar-base
more and shouldn't mix presentation and model layers. We can talk about this tomorrow.
src/actions/xdrViewer.js
Outdated
network.networkId(), | ||
xdr.EnvelopeType.envelopeTypeTx().toXDR(), | ||
txXdr | ||
])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything above can be changed to:
let tx = new StellarSdk.Transaction(input);
const signatureBase = tx.signatureBase();
src/actions/xdrViewer.js
Outdated
// We define a verification as ternary: | ||
// 0 - no verification | ||
// 1 - false verification | ||
// 2 - true verification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be constants like:
const SIGNATURE_NOT_VERIFIED_YET = 0;
const SIGNATURE_INVALID = 1;
const SIGNATURE_VALID = 1;
src/actions/xdrViewer.js
Outdated
.catch(err => dispatch({ | ||
type: FETCHED_SIGNERS_FAIL, | ||
})); | ||
} catch(error) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be handled as well.
src/actions/xdrViewer.js
Outdated
}); | ||
|
||
return srcAccounts; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should leverage stellar-base
more here. When you create Transaction
object like:
let tx = new Transaction(xdrObject);
you have:
- transaction source in
tx.source
, - each operation source in
tx.operations[i].source
.
src/actions/xdrViewer.js
Outdated
// Utilize same strategy as ExtrapolateXDR file, reference there for how this works | ||
const hintBytes = new Buffer(x._attributes.hint.toString("base64"), "base64"); | ||
const partialPublicKey = Buffer.concat([new Buffer(28).fill(0), hintBytes]); | ||
const hint = StrKey.encodeEd25519PublicKey(partialPublicKey, 'base64').substr(47, 5); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we shouldn't do it. We're mixing presentation and model layers. Why not compare raw bytes? If you check this big comment in extrapolateFromXdr.js
file about hints you'll see that the base58 representation of a hint is actually missing a few bits of data because of the way base58 works:
// byte 28: #### #### full b32 symbols
// byte 29: # ##### ## |--------------------------|
// byte 30: ### 48### |
// byte 31: Signature Hint start | 49### 50# | Signature Hint end
// byte 32: ## 51### 5 | |
// byte 33: 2### 53##
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to ignore the hint. In my discussion with Tomer last week, we don't need to utilize the hint optimization for this.
src/actions/xdrViewer.js
Outdated
resp.data.signers.forEach(signer => { | ||
// Get key hint for matching | ||
const key = signer.key.substring(signer.key.length-9, signer.key.length-4); | ||
if (!obj.hasOwnProperty(key)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if there are two keys with the same hint? Hint is only 4 bytes so it's likely to happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe stellar core uses this for optimization -- some trade off between likely-hood of collision vs space saved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to below, decided to ignore the hint.
@bartekn thanks for the feedback. Utilizing Stellar-base and removing trivial hint optimization lead to much cleaner code. Also squashed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, in this review I focused on model layer, next week will check presentation layer more.
src/actions/xdrViewer.js
Outdated
return dispatch => { | ||
try { | ||
// Capture network for determining signature base | ||
StellarSdk.Network.use(new Network(networkPassphrase)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: we should fix stellar/js-stellar-base#112 :)
src/actions/xdrViewer.js
Outdated
if (!sourceAccounts.hasOwnProperty(key)) { | ||
sourceAccounts[key] = true; | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we exchange the function above with:
if (op.source) {
sourceAccounts[op.source] = true;
}
3 vs 7 lines, doing the same stuff :).
src/actions/xdrViewer.js
Outdated
switch (signer.type) { | ||
case 'sha256_hash': | ||
// Convert to string since you can't compare two buffer arrays | ||
isValid = signer.value.toString() === sigObj.hashx.toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it won't work. signer.value
is taken directly from Horizon API in line 65 and the key is in strkey
format: https://horizon-testnet.stellar.org/accounts/GADKHRMUM76DVVW5M6FESVQ4DHYLJN42LNJXTVZDNABBPEXDA2AJWTRY. So you should use StrKey.decodeSha256Hash
(or encodeSha256Hash
somewhere else) function.
Also, don't use toString
, check my other comment. Use Buffer.equals
or compare byte by byte.
src/actions/xdrViewer.js
Outdated
const signature = x._attributes.signature; | ||
return { | ||
// In case of hashX | ||
hashx: hash(signature.toString()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't use toString()
here. More info: nodejs/node#3982
src/actions/xdrViewer.js
Outdated
keypair: Keypair.fromPublicKey(signer.key), | ||
}; | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it will be much easier to read after changing this function to a simple one-liner:
resp.data.signers.forEach(signer => obj[signer.key] = signer);
and do all necessary conversions from one type to another in the loops below. Currently conversions happen here and there and it complicates everything.
|
||
if (isValid) { | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if
is redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure this is invalid, at least the way the code is currently written. Here the check on isValid
exits the loop once a valid key is found. Not only does this optimize (skipping a few rounds of checks once a valid key is seen) it also breaks since isValid
is re-written during each pass of the internally nested loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, my mistake.
src/actions/xdrViewer.js
Outdated
// In case of hashX | ||
hashx: hash(signature.toString()), | ||
// Used for matching against ExtrapolateXDR's signature formatting | ||
value: new Buffer(signature).toString('base64'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to have all low level binary data as Buffer
and convert when needed. Currently it's really hard to remember which values are buffers/hex/byte64.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes, nicolas opened an issue about that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed: as you said above, I kept signature as base64 as it is in TreeViewer then convert it back to binary and compare that signature against my fetchSigners list (buff.equals).
Fixed per your latest comments... you were right, hash(x) still failed under the previous commit. It should be fixed now with buffer/hex/bin sorted out a bit more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/actions/xdrViewer.js
looks good. Added comments to presentation layer.
src/actions/xdrViewer.js
Outdated
const hashedSignatureBase = hash(tx.signatureBase()); | ||
|
||
// Extract all signatures on transaction | ||
const signatures = tx.signatures.map(x => ({ sig: x._attributes.signature })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I missed that one. js-xdr
lacks good documentation but I think it has autogenerated getter functions so instead x._attributes.signature
we should use x.signature()
or x.get("signature")
whichever works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is x.signature()... much nicer 😄
src/reducers/xdrViewer.js
Outdated
@@ -37,3 +37,13 @@ function type(state = 'TransactionEnvelope', action) { | |||
|
|||
return state; | |||
} | |||
|
|||
function fetchedSigners(state = "", action) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't default state
be []
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed default to null -- I want a way to make sure this is run at least once, say if you were to copy-paste a link with xdr I want this to trigger
src/components/TreeView.js
Outdated
let rootClass = 'TreeView ' + (className) ? className : ''; | ||
|
||
// First check in case of transaction with no signatures, i.e. pre-auth | ||
if(nodes[0] && nodes[0].type == "hint" && fetchedSigners) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this condition is a little complicated. fetchedSigners
is present if the type is a TransactionEnvelope
(checked in state.type === "TransactionEnvelope"
condition). So we really want to check if we are in TransactionEnvelope
view but we're doing it by checking the other variable set somewhere else.
Also I believe that in a very near future we'll want to fix #336 and it will be much easier to do if we know the exact location we're currently at in a tree.
So I propose to construct a location string. We always have key name in nodes[i].type
. So we can construct a location string like TransactionEnvelope.signatures[i]
by appending the current key name in each child and trigger an action when we are when we want to be. For location with a variable part ( arrays like TransactionEnvelope.signatures[0]
, TransactionEnvelope.signatures[1]
...) we can use RegExp
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite following here -- can discuss tomorrow AM (UTC -7).
I see the key name, but where does the location string live? Is it attached to the node? I know that the TreeView has some recursion, so does this key persist through iterations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to build a location ourselves. TreeView
renders itself recursively so if we send a current (then parent) name to the child we can construct the full location string. Check the quick patch below:
diff --git a/src/components/TreeView.js b/src/components/TreeView.js
index e7ed97b..8a0e4d4 100644
--- a/src/components/TreeView.js
+++ b/src/components/TreeView.js
@@ -9,7 +9,7 @@ import SIGNATURE from '../constants/signature';
// @param {array} props.nodes - Array of TreeView compatible nodes
export default class TreeView extends React.Component {
render() {
- let {nodes, className, fetchedSigners} = this.props;
+ let {nodes, className, fetchedSigners, parent} = this.props;
let rootClass = 'TreeView ' + (className) ? className : '';
if(nodes[1] && nodes[1].type == "signatures") {
@@ -20,15 +20,22 @@ export default class TreeView extends React.Component {
{_.map(Array.prototype.slice.call(nodes), (node, index) => {
let childNodes;
+ let sep = '.';
+ if (!parent || node.type.charAt(0) == '[') {
+ sep = '';
+ }
+
+ let position = (parent ? parent+sep : '') + node.type;
+
if (typeof node.nodes !== 'undefined') {
childNodes = <div className="TreeView__child">
- <TreeView nodes={node.nodes} fetchedSigners={fetchedSigners}/>
+ <TreeView nodes={node.nodes} fetchedSigners={fetchedSigners} parent={position} />
</div>;
}
return <div className="TreeView__set" key={index}>
<div className="TreeView__row" key={index + node.type}>
- <RowValue node={node} />
+ <RowValue node={node} position={position} />
</div>
{childNodes}
</div>
@@ -67,7 +74,7 @@ function checkSignatures(nodes, fetchedSigners) {
function RowValue(props) {
let value, childNodes, separatorNeeded, separator;
- let {node} = props;
+ let {node, position} = props;
if (typeof node.value === 'string') {
value = String(node.value);
@@ -95,7 +102,7 @@ function RowValue(props) {
return formatSignature(node, separator);
}
- return <span><strong>{node.type}</strong>{separator}{value}</span>
+ return <span><strong>{node.type} {position}</strong>{separator}{value}</span>
}
// Types values are values that will be displayed with special formatting to
Then instead of:
if(nodes[1] && nodes[1].type == "signatures")
We can check if we are in the exact location in a tree:
if (position == 'TransactionEnvelope.signatures')
src/components/TreeView.js
Outdated
let isValid = SIGNATURE.INVALID; | ||
if (sig) { | ||
isValid = sig.isValid; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isValid
defaults to SIGNATURE.INVALID
so in case of errors (ex. connection error) the signature will be displayed as invalid (you can go to Chrome Dev tools, switch to Offline mode and load a TransactionEnvelope
to reproduce it).
What do you think something like this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! So basically a sort of visual of the state of fetched signatures:
- loading
- failed
- success (same as now)
src/components/TreeView.js
Outdated
@@ -34,6 +40,18 @@ export default class TreeView extends React.Component { | |||
} | |||
} | |||
|
|||
function checkSignature(nodes, fetchedSigners) { | |||
const signatureBuffer = Buffer.from(nodes[1].value.value, 'base64'); | |||
const sig = fetchedSigners.filter(x => x.sig.equals(signatureBuffer))[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to use find
instead of filter
here.
src/components/XdrViewer.js
Outdated
@@ -39,13 +44,18 @@ function XdrViewer(props) { | |||
<p>The XDR Viewer is a tool that displays contents of a Stellar XDR blob in a human readable format.</p> | |||
</div> | |||
<p className="XdrViewer__label"> | |||
Input a base-64 encoded XDR blob, or <a onClick={() => dispatch(fetchLatestTx(baseURL))}>fetch the latest transaction to try it out</a>: | |||
Input a base-64 encoded XDR blob, or <a onClick={() => dispatch(fetchLatestTx(baseURL))}>fetch the latest transaction to try it out</a>: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you click this link it doesn't trigger signers check.
Fixed for presentation level feedback. However, left a reply on a comment where I was a bit confused. |
src/components/TreeView.js
Outdated
function checkSignatures(nodes, fetchedSigners) { | ||
// catch fetch signature errors | ||
if (fetchedSigners === null) { | ||
nodes[1].state = SIGNATURE.NOT_VERIFIED_YET; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think something is wrong here. I set connection latency to 10s in devtools. I expected to see "Checking signatures..." but instead I got "Error checking signatures..." and after 10s it disappeared and updated each signature state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see. The error is that I have an initial state of null with only two states, fail and success. However, there is in fact also a third state (different from initial state) for loading.
src/components/TreeView.js
Outdated
let position = (parent ? parent + sep : '') + node.type; | ||
|
||
if (position == 'TransactionEnvelope.signatures') { | ||
checkSignatures(nodes, fetchedSigners); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use node
here so that you don't need to use nodes[1]
inside checkSignatures
..
src/components/TreeView.js
Outdated
@@ -56,6 +96,14 @@ function RowValue(props) { | |||
separator = ': '; | |||
} | |||
|
|||
if (node.type === 'signatures') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use position
prop here.
src/components/TreeView.js
Outdated
return formatSignatureCheckState(node, separator) | ||
} | ||
|
||
if (node.type === "signature") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use position
prop here too but use regexp to check if you are in TransactionEnvelope.signatures[x].signature
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the final round of comments. Will merge right away after these changes. Good job 👍 !
src/components/TreeView.js
Outdated
sep = ''; | ||
} | ||
|
||
let position = (parent ? parent + sep : '') + node.type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move it to a separate function.
src/reducers/xdrViewer.js
Outdated
@@ -37,3 +37,15 @@ function type(state = 'TransactionEnvelope', action) { | |||
|
|||
return state; | |||
} | |||
|
|||
function fetchedSigners(state = "uninitialized", action) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use undefined
here (or even remove value as it will be undefined
by default?). Or maybe add:
export const FETCHED_SIGNERS_PENDING = 'FETCHED_SIGNERS_PENDING'; // or a better name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally that was undefined, specifically an empty string, however in js land "" == []
which failed for this:
// Fetch signers on initial load
if (state.type === "TransactionEnvelope" && state.fetchedSigners === 'uninitialized') {
dispatch(fetchSigners(state.input, baseURL, networkPassphrase))
}
since initial state == case with no signers ([])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, default state cannot be undefined:
combineReducers.js:56 Uncaught Error: Reducer "fetchedSigners" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to go with:
INITIAL --> null
SUCCESS --> result (type array)
FAILURE --> "ERROR"
PENDING --> "PENDING"
src/components/TreeView.js
Outdated
} else if (node.value.isValid === SIGNATURE.VALID) { | ||
style = {color: "green"}; | ||
} | ||
return <span style={style}><strong>{node.type}</strong>{separator}{node.value.value}</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add add a small icon here too, like ❌ / ✅ (can be unicode).
src/constants/signature.js
Outdated
@@ -0,0 +1,9 @@ | |||
// This standardizes all the slug strings into a simple map | |||
// These are used in to build the url and are the "page name" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment seems wrong.
src/components/TreeView.js
Outdated
return formatSignatureCheckState(node, separator) | ||
} | ||
|
||
if (position.match(/(\.signature)+$/)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We really need a full regexp here. No idea how TransactionEnvelope
will looks like in a future and it will break if there's another signature
field.
src/actions/xdrViewer.js
Outdated
}) | ||
.catch(err => dispatch({ type: FETCHED_SIGNERS_FAIL })); | ||
} catch(error) { | ||
dispatch({ type: FETCHED_SIGNERS_FAIL }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add console.error
in both catch
functions.
src/components/TreeView.js
Outdated
return <span><strong>{node.type}</strong>{separator}{value}</span> | ||
} | ||
|
||
function checkSignatures({signatures, fetchedSigners}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for destructuring here and other methods? I think it's just copied from convertTypedValue({type, value})
definition where it's helpful because you can pass a node value there: convertTypedValue(node.value)
.
src/components/TreeView.js
Outdated
signatures.state = SIGNATURE.VALID; | ||
|
||
for (var i = 0; i < signatures.nodes.length; i ++) { | ||
const sig = signatures.nodes[i].nodes[1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be confusing if you don't remember that signatures.nodes
are decorated signatures. Let's change it to:
const sig = signatures.nodes[i].find(n => n.type == 'signature');
src/components/TreeView.js
Outdated
|
||
for (var i = 0; i < signatures.nodes.length; i ++) { | ||
const sig = signatures.nodes[i].nodes[1]; | ||
const signatureBuffer = Buffer.from(sig.value.value, 'base64'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a part of a separate PR we should probably move all the extra presentation functions here from extrapolateFromXdr
. Right now we should probably leave the original value in a resulting object like:
if (object && object._isBuffer) {
return {type: 'code', raw: object, value: new Buffer(object).toString('base64')};
}
Without it we need to convert it back to bytes from base64 representation. Then we can do:
const fetchedSignature = fetchedSigners.find(x => x.sig.equals(sig.raw));
@bartekn ok updated for latest comments. Also squashed. |
This may need some work making it proper React code. It also might need some touchup on design. I kept it pretty simple.
Basically, whenever the user types or pastes in an xdr of type
Transaction Envelope
, this will asynchronously query for all signers on that transaction (base source account and all operation source accounts). It will then, for each signature on the transaction check to see if it is valid for a given signer -- the red means invalid, green means valid, and during loading, black means not yet loaded.Here is what it looks like for a transaction has two valid signers and one invalid signer:
Closes #343