Unable to verify message signed by sol-wallet-adapter - ecdsa

Having created a signed message I'm unsure how to use the resulting signature to verify the message using the publicKey.
My use case is, I'm wanting to use a Solana Wallet to login to an API server with a pattern like:
GET message: String (from API server)
sign message with privateKey
POST signature (to API server)
verify signature with stored publicKey
I've attempted to use nodeJS crypto.verify to decode the signed message on the API side but am a bit out of my depth digging into Buffers and elliptic curves:
// Front-end code
const toHexString = (buffer: Buffer) =>
buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
const data = new TextEncoder().encode('message to verify');
const signed = await wallet.sign(data, "hex");
await setLogin({ // sends API post call to backend
variables: {
publicAddress: walletPublicKey,
signature: toHexString(signed.signature),
},
});
// Current WIP for backend code
const ALGORITHM = "ed25519";
const fromHexString = (hexString) =>
new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
const signature = fromHexString(args.signature);
const nonceUint8 = new TextEncoder().encode('message to verify');
const verified = crypto.verify(
ALGORITHM,
nonceUint8,
`-----BEGIN PUBLIC KEY-----\n${user.publicAddress}\n-----END PUBLIC KEY-----`,
signature
);
console.log("isVerified: ", verified);
I'm pretty sure I'm going about this the wrong way and there must be an obvious method I'm missing.
As the space matures I expect a verify function or lib will appear to to consume the output of const signed = await wallet.sign(data, "hex");
Something like:
import { VerifyMessage } from '#solana/web3.js';
const verified = VerifyMessage(message, publicKey, signature, 'hex');
But after 3 days of pushing hard I'm starting to hit my limits and my brain is failing. Any help or direction where to look much appreciated 🙏

Solved with input from the fantastic Project Serum discord devs. High level solution is to use libs that are also used in the sol-wallet-adapter repo, namely tweetnacl and bs58:
const signatureUint8 = base58.decode(args.signature);
const nonceUint8 = new TextEncoder().encode(user?.nonce);
const pubKeyUint8 = base58.decode(user?.publicAddress);
nacl.sign.detached.verify(nonceUint8, signatureUint8, pubKeyUint8)
// true

I recommend staying in solana-labs trail and use tweetnacl
spl-token-wallet (sollet.io) signs an arbitrary message with
nacl.sign.detached(message, this.account.secretKey)
https://github.com/project-serum/spl-token-wallet/blob/9c9f1d48a589218ffe0f54b7d2f3fb29d84f7b78/src/utils/walletProvider/localStorage.js#L65-L67
on the other end, verify is done with
nacl.sign.detached.verify
in #solana/web3.js
https://github.com/solana-labs/solana/blob/master/web3.js/src/transaction.ts#L560
Use nacl.sign.detached.verify in your backend and you should be good. I also recommend avoiding any data format manipulation, I am not sure what you were trying to do but if you do verify that each step is correct.

For iOS, solana.request will cause error. Use solana.signMessage and base58 encode the signature.
var _signature = '';
try {
signedMessage = await window.solana.request({
method: "signMessage",
params: {
message: encodedMessage
},
});
_signature = signedMessage.signature;
} catch (e) {
try {
signedMessage = await window.solana.signMessage(encodedMessage);
_signature = base58.encode(signedMessage.signature);
} catch (e1) {
alert(e1.message);
}
}
//
try {
signIn('credentials',
{
publicKey: signedMessage.publicKey,
signature: _signature,
callbackUrl: `${window.location.origin}/`
}
)
} catch (e) {
alert(e.message);
}

I needed to convert Uint8Array to string and convert it back to Uint8Array for HTTP communication. I found the toLocaleString method of Uint8Array helpful in this case. It outputs comma-separated integers as a string.
const signedMessage = await window.solana.signMessage(encodedMessage, "utf8");
const signature = signedMessage.signature.toLocaleString();
And then you can convert it back to Uint8Array with the following code.
const signatureUint8 = new Uint8Array(signature.split(",").map(Number));
Edit
The solution above was working on the desktop but when I tried my code inside the Phantom wallet iOS browser it gave an error. I guess the toLocaleString method is not available in that browser. I found a more solid solution to convert Uint8Array to a comma-separated string
Array.apply([], signedMessage.signature).join(",")

Signing and base64 encode:
const data = new TextEncoder().encode(message);
const signature = await wallet.signMessage(data); // Uint8Array
const signatureBase64 = Buffer.from(signature).toString('base64')
Base64 decode and verifying:
const signatureUint8 = new Uint8Array(atob(signature).split('').map(c => c.charCodeAt(0)))
const messageUint8 = new TextEncoder().encode(message)
const pubKeyUint8 = wallet.publicKey.toBytes() // base58.decode(publicKeyAsString)
const result = nacl.sign.detached.verify(messageUint8, signatureUint8, pubKeyUint8) // true or false
Full code example: https://github.com/enginer/solana-message-sign-verify-example

Related

How do I await a .wasm (WebAssembly file) from a Rest API?

I am making a Next.js app that has a page that connects to a REST API. The REST API simply spits out a raw .wasm (WebAssembly) file. I know that sounds atypical but it's 100% necessary for my app. I have this function which is supposed to fetch the .wasm file from the API:
const fetchedWasmWrapped = useCallback( async () => {
const endpoint = "/.netlify/functions" + "/decrypt?authSig="+JSON.stringify(props.props.authSig);
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/wasm',
},
};
const response = await fetch(endpoint, options);
const result = await response.text();
console.log(result);
setDecryptedWasm(result);
}, [props.props.authSig])
The function works. It DOES output the .wasm file but console.log(result) writes a bizarre string with a lot of symbols and empty characters. I believe the reason why result is all jumbled up is because I don't think WebAssembly is meant to be printed as a string. It's actually a blob. result is then read as a file later in my app but that fails and I'm assuming it's because the WebAssembly code was not imported as a blob but rather a string.
So how do I tell typescript that I want the ".wasm" code from response? I don't believe there a response.wasm() function so how do I do this?
The key is that you have to change const result = await response.text(); to const result = await response.blob();
This threw more errors in my vscode terminal because setDecryptedWasm was expecting a string. So change const [decryptedWasm, setDecryptedWasm] = useState(''); to const [decryptedWasm, setDecryptedWasm] = useState(new File([], ''));
And rather than setting decryptedWasm with setDecryptedWasm(result), do setDecryptedWasm(new File([result], 'myunityapp.wasm'));

How to send Signature (r,s,v) to smart contract external function?

I need to execute an external function at a deployed smart contract. This function receives a "Signature" struct as a parameter.
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}
I've checked at https://medium.com/#angellopozo/ethereum-signing-and-validating-13a2d7cb0ee3
, the following code:
const Web3 = require('web3')
const provider = new Web3.providers.HttpProvider('http://localhost:8545')
const web3 = new Web3(provider)
function toHex(str) {
var hex = ''
for(var i=0;i<str.length;i++) {
hex += ''+str.charCodeAt(i).toString(16)
}
return hex
}
let addr = web3.eth.accounts[0]
let msg = 'I really did make this message'
let signature = web3.eth.sign(addr, '0x' + toHex(msg))
console.log(signature)
What should we set at "msg" in this case to call the function?
The article you shared is about verifying the account which does the transaction:
Signing is a nice way to know something is being done by the correct
person/contract. This means we can trust that someone is actually
doing what they say they are.
Especially in NFT marketplaces before you upload metadata or images you are required to verify that your metamask account will the one who really does the transactions. To verify you just sign a message on the front-end and sent this signed message to the server. This msg can be anything, its value is not important, its signature is important. Once you receive the msg, you can verify it. I will show you the verification process, so you will extract the v,r,s and then pass them to your contract function if the verification process is successful. You should be also building the contract on the server as well.
import * as util from "ethereumjs-util";
import { ethers } from "ethers";
// you could send this with session as well
const message = req.body.message;
// getting contract on serverside. I am setting with ganache
const provider = new ethers.providers.JsonRpcProvider(
"http://127.0.0.1:7545"
);
// import your abi and deployed smart contract address
const contract = new ethers.Contract(
contractAddress,
abi,
provider
)
// ****** Verification Process Starts ********
// we need to get the unsigned message
// nonce is the representation of something that we are going to sign
let nonce: string | Buffer =
"\x19Ethereum Signed Message:\n" +
JSON.stringify(message).length +
JSON.stringify(message);
// create a new buffer
nonce = util.keccak(Buffer.from(nonce, "utf-8"));
// signature: signedData, from signature we will get the address
const { v, r, s } = util.fromRpcSig(req.body.signature);
// matching signature with the unsigned message
const pubKey = util.ecrecover(util.toBuffer(nonce), v, r, s);
const addressBuffer = util.pubToAddress(pubKey);
const address = util.bufferToHex(addressBuffer);
if (address === req.body.address) {
// here call your contract external address passing "v,r,s"
} else {
reject("Validation Failed");
}

Wrap JWS in another JWS

I have a JWT which is created by a source server. Before the JWT goes to the destination server it passes through another intermediate service which verifies, adds some extra data and signs. I need the destination to verify it came from both services and obtain the extra_data
I was thinking the final JWT will look like the following
const token = JWT({
sub: 'ey....,
extra_data: 123,
})
and the code I will use to verify and get data would look like:
const { sub, extra_data } = jwt.verify(token, publicKeyIntermediate);
const { data } = jwt.verify(sub, publicKeySource);
const finalPayload = {
extra_data,
...data,
}
Is this the correct way to deal with this situation?

jsonwebtoken package fails to verify user Office.context.mailbox.getUserIdentityToken result

I'm developing an Outlook web add-in.
I'm trying to verify a token that I'm passing to server side, through a node.js library, but it's failing and I can't understand why.
This is what I'm doing to retrieve the user identity token.
Office.context.mailbox.getUserIdentityTokenAsync(function(result) {
result.value // contains the token.
// send this value to server side, which I can see that it's working.
})
On the server side, I retrieve the token and do below:
token; // contains the token passed from the web-app.
const jwt = require('jsonwebtoken');
const request = require('request');
let decoded = jwt.decode(token, {complete: true});
// Get the key, we'll need this later since we'll have to
// find the matching key from the config file.
let key = decoded.header.x5t;
let amurl = JSON.parse(decoded.payload.appctx).amurl;
// Make a request to the url to get the configuration json file.
request(amurl, {}, (err, response, body) => {
let keys = JSON.parse(body).keys;
// Filter the keys so we get the one which we can verify.
let s = keys.filter(t => t.keyinfo.x5t === key);
let cert = s[0].keyvalue.value;
// Fails the verification.
console.log(jwt.verify(token, cert));
});
Just to clarify, I'm retrieving the token correctly and this npm package seems to be functioning fine for other jwt tokens. (So it's not really a configuration issue)
I have now found the answer to this question.
Just to re-iterate the problem was:
Office.context.mailbox.getUserIdentityToken method returns a jwt token.
When decoded this token contains an amurl field which points to the public certificate as a text.
When jsonwebtoken.verify(token, certText) is called, it's failing with the message invalid algorithm (even if you specify the algorithm from the token's header)
The problem was the formatting of the certificate text. jsonwebtoken package was looking for a particular formatting (splitted with 64 characters across each line along with the certificate begin and certificate end lines, so when formatted with method below - it started working properly.
The original code is taken from here: https://github.com/auth0/node-jsonwebtoken/issues/68 and formatted slightly to fit the needs.
/**
* #param {string} key - The certificate value retrieved from amurl property.
*/
formatKey: function(key) {
const beginKey = "-----BEGIN CERTIFICATE-----";
const endKey = "-----END CERTIFICATE-----";
const sanitizedKey = key.replace(beginKey, '').replace(endKey, '').replace('\n', '')
const keyArray = sanitizedKey.split('').map((l, i) => {
const position = i + 1
const isLastCharacter = sanitizedKey.length === position
if(position % 64 === 0 || isLastCharacter) {
return l + '\n'
}
return l
})
return `${beginKey}\n${keyArray.join('')}${endKey}\n`
}

Conversion of Facebook payload to sha1 value to check and match with x-hub-signation

I am trying to implement facebook webhook security.
The below code works fine for text messages but the moment attachments are sent , the sha value doesnot match.
I tried calculating on the escaped Unicode lowercase payload but then ended up having different sha value for simple texts as well.
Any help will be greatly appreciated .
byte[] payloadBytes = request.inputStream.bytes
String hashReceived = xHubSignature.substring(5)
String hashComputed = HmacUtils.hmacSha1Hex(facebookAppSecret.getBytes(StandardCharsets.UTF_8), payloadBytes)
log.info('Received {} computed {}', hashReceived, hashComputed)
Turns out the problem was in the way I was accessing the data, something like this:
var express = require('express');
var app = express()
// Other imports
app.listen(app.get('port'), ()=>
{console.log('running on port', app.get('port'))});
The request body was accessed like this:
app.post('/webhook/', (req, res)=>
{
let body = req.body;
//By this time the encoded characters were already decoded and hence the hashcheck was failing.
//processing the data
});
The solution was to use natice httpServer to create the server and access the data so that the hashcheck was done on the raw data.
Probably this can be done using Express as well but it was not working for me.
This is what I did.
const http = require('http');
const url = require("url");
const crypto = require('crypto');
http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
let body = '';
req.on('data', chunk => {
body += chunk
});
if (urlItems.pathname === '/facebook/' && req.method === 'POST') {
req.on('end', () => {
let hmac = crypto.createHmac('sha1', appSecret);
hmac.update(body, 'utf-8');
let computedSig = `sha1=${hmac.digest('hex')}`;
if (req.headers['x-hub-signature'] === computedSig) {
console.log(`${computedSig} matched ${req.headers['x-hub-signature']}`);
} else {
console.log(`Found ${computedSig} instead of ${req.headers['x-hub-signature']}`);
}
res.end(JSON.stringify({ status: 'ok' }))
})
}
}).listen(process.env.PORT || 3000);
EDIT 1 : Due change in infra , we switched to Node hence the node code.