-
Notifications
You must be signed in to change notification settings - Fork 137
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 helpers method to build authorization entries. #663
Merged
+310
−7
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b7f7bc9
Add helper to build auth entries
Shaptic 5eda0b4
Separate helper into two methods
Shaptic f3908ae
Fixup imports and whatnot
Shaptic 4326115
Fixups after writing tests
Shaptic bd1ebba
Prettier fixup
Shaptic cc42e7f
Add async callback-based method
Shaptic 8cca8c9
Fixups after adding tests
Shaptic a47ff68
Remove unused jsdoc
Shaptic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
import xdr from './xdr'; | ||
|
||
import { StrKey } from './strkey'; | ||
import { Keypair } from './keypair'; | ||
import { hash } from './hashing'; | ||
|
||
import { Address } from './address'; | ||
import { nativeToScVal } from './scval'; | ||
|
||
/** | ||
* This builds an authorization entry that indicates to | ||
* {@link Operation.invokeHostFunction} that a particular identity (i.e. signing | ||
* {@link Keypair} or other signer) approves the execution of an invocation tree | ||
* (i.e. a simulation-acquired {@link xdr.SorobanAuthorizedInvocation}) on a | ||
* particular network (uniquely identified by its passphrase, see | ||
* {@link Networks}) until a particular ledger sequence is reached. | ||
* | ||
* This enables building an {@link xdr.SorobanAuthorizationEntry} without | ||
* worrying about how to combine {@link buildAuthEnvelope} and | ||
* {@link buildAuthEntry}, while those allow advanced, asynchronous, two-step | ||
* building+signing of the authorization entries. | ||
* | ||
* This one lets you pass a either a {@link Keypair} or a callback function to | ||
* handle signing the envelope hash. | ||
* | ||
* @param {Keypair} signer the identity keypair authorizing this invocation | ||
* @param {string} networkPassphrase the network passphrase is incorprated | ||
* into the signature (see {@link Networks} for options) | ||
* @param {number} validUntil the (exclusive) future ledger sequence number | ||
* until which this authorization entry should be valid (if | ||
* `currentLedgerSeq==validUntil`, this is expired)) | ||
* @param {xdr.SorobanAuthorizedInvocation} invocation the invocation tree that | ||
* we're authorizing (likely, this comes from transaction simulation) | ||
* | ||
* @returns {xdr.SorobanAuthorizationEntry} an authorization entry that you can | ||
* pass along to {@link Operation.invokeHostFunction} | ||
*/ | ||
export function authorizeInvocation( | ||
signer, | ||
networkPassphrase, | ||
validUntil, | ||
invocation | ||
) { | ||
const preimage = buildAuthEnvelope(networkPassphrase, validUntil, invocation); | ||
const input = hash(preimage.toXDR()); | ||
const signature = signer.sign(input); | ||
return buildAuthEntry(preimage, signature, signer.publicKey()); | ||
} | ||
|
||
/** | ||
* This works like {@link authorizeInvocation}, but allows passing an | ||
* asynchronous callback as a "signing method" (e.g. {@link Keypair.sign}) and a | ||
* public key instead of a specific {@link Keypair}. | ||
* | ||
* This is to make two-step authorization (i.e. custom signing flows) easier. | ||
* | ||
* @borrows authorizeInvocation | ||
* | ||
* @param {string} publicKey the public identity that is authorizing this | ||
* invocation via its signature | ||
* @param {function(Buffer): Buffer} signingMethod a function which takes | ||
* an input bytearray and returns its signature as signed by the private key | ||
* corresponding to the `publicKey` parameter | ||
* @param {string} networkPassphrase the network passphrase is incorprated | ||
* into the signature (see {@link Networks} for options) | ||
* @param {number} validUntil the (exclusive) future ledger sequence number | ||
* until which this authorization entry should be valid (if | ||
* `currentLedgerSeq==validUntil`, this is expired)) | ||
* @param {xdr.SorobanAuthorizedInvocation} invocation the invocation tree that | ||
* we're authorizing (likely, this comes from transaction simulation) | ||
* | ||
* @param {xdr.SorobanAuthorizedInvocation} invocation | ||
* | ||
* @returns {Promise<xdr.SorobanAuthorizationEntry>} | ||
* @see authorizeInvocation | ||
*/ | ||
export async function authorizeInvocationCallback( | ||
publicKey, | ||
signingMethod, | ||
networkPassphrase, | ||
validUntil, | ||
invocation | ||
) { | ||
const preimage = buildAuthEnvelope(networkPassphrase, validUntil, invocation); | ||
const input = hash(preimage.toXDR()); | ||
const signature = await signingMethod(input); | ||
return buildAuthEntry(preimage, signature, publicKey); | ||
} | ||
|
||
/** | ||
* Builds an {@link xdr.HashIdPreimage} that, when hashed and signed, can be | ||
* used to build an {@link xdr.SorobanAuthorizationEntry} via | ||
* {@link buildAuthEnvelope} to approve {@link Operation.invokeHostFunction} | ||
* invocations. | ||
* | ||
* The envelope built here will approve the execution of an invocation tree | ||
* (i.e. a simulation-acquired {@link xdr.SorobanAuthorizedInvocation}) on a | ||
* particular network (uniquely identified by its passphrase, see | ||
* {@link Networks}) until a particular ledger sequence is reached (exclusive). | ||
* | ||
* @param {string} networkPassphrase the network passphrase is incorprated | ||
* into the signature (see {@link Networks} for options) | ||
* @param {number} validUntil the (exclusive) future ledger sequence number | ||
* until which this authorization entry should be valid | ||
* @param {xdr.SorobanAuthorizedInvocation} invocation the invocation tree that | ||
* we're authorizing (likely, this comes from transaction simulation) | ||
* | ||
* @returns {xdr.HashIdPreimage} a preimage envelope that, when hashed and | ||
* signed, represents the signature necessary to build a proper | ||
* {@link xdr.SorobanAuthorizationEntry} via {@link buildAuthEntry}. | ||
*/ | ||
export function buildAuthEnvelope(networkPassphrase, validUntil, invocation) { | ||
// We use keypairs as a source of randomness for the nonce to avoid mucking | ||
// with any crypto dependencies. Note that this just has to be random and | ||
// unique, not cryptographically secure, so it's fine. | ||
const kp = Keypair.random().rawPublicKey(); | ||
const nonce = new xdr.Int64(bytesToInt64(kp)); | ||
|
||
const networkId = hash(Buffer.from(networkPassphrase)); | ||
const envelope = new xdr.HashIdPreimageSorobanAuthorization({ | ||
networkId, | ||
invocation, | ||
nonce, | ||
signatureExpirationLedger: validUntil | ||
}); | ||
|
||
return xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(envelope); | ||
} | ||
|
||
/** | ||
* Builds an auth entry with a signed invocation tree. | ||
* | ||
* You should first build the envelope using {@link buildAuthEnvelope}. If you | ||
* have a signing {@link Keypair}, you can use the more convenient | ||
* {@link authorizeInvocation} to do signing for you. | ||
* | ||
* @param {xdr.HashIdPreimage} envelope an envelope to represent the call tree | ||
* being signed, probably built by {@link buildAuthEnvelope} | ||
* @param {Buffer|Uint8Array} signature a signature of the hash of the | ||
* envelope by the private key corresponding to `publicKey` (in other words, | ||
* `signature = sign(hash(envelope))`) | ||
* @param {string} publicKey the public identity that signed this envelope | ||
* | ||
* @returns {xdr.SorobanAuthorizationEntry} | ||
* | ||
* @throws {Error} if `verify(hash(envelope), signature, publicKey)` does not | ||
* pass, meaning one of the arguments was not passed or built correctly | ||
* @throws {TypeError} if the envelope does not hold an | ||
* {@link xdr.HashIdPreimageSorobanAuthorization} instance | ||
*/ | ||
export function buildAuthEntry(envelope, signature, publicKey) { | ||
// ensure this identity signed this envelope correctly | ||
if ( | ||
!Keypair.fromPublicKey(publicKey).verify(hash(envelope.toXDR()), signature) | ||
) { | ||
throw new Error(`signature does not match envelope or identity`); | ||
} | ||
|
||
if ( | ||
envelope.switch() !== xdr.EnvelopeType.envelopeTypeSorobanAuthorization() | ||
) { | ||
throw new TypeError( | ||
`expected sorobanAuthorization envelope, got ${envelope.switch().name}` | ||
); | ||
} | ||
|
||
const auth = envelope.sorobanAuthorization(); | ||
return new xdr.SorobanAuthorizationEntry({ | ||
rootInvocation: auth.invocation(), | ||
credentials: xdr.SorobanCredentials.sorobanCredentialsAddress( | ||
new xdr.SorobanAddressCredentials({ | ||
address: new Address(publicKey).toScAddress(), | ||
nonce: auth.nonce(), | ||
signatureExpirationLedger: auth.signatureExpirationLedger(), | ||
signatureArgs: [ | ||
nativeToScVal( | ||
{ | ||
public_key: StrKey.decodeEd25519PublicKey(publicKey), | ||
signature | ||
}, | ||
{ | ||
// force the keys to be interpreted as symbols (expected for | ||
// Soroban [contracttype]s) | ||
type: { | ||
public_key: ['symbol', null], | ||
signature: ['symbol', null] | ||
} | ||
} | ||
) | ||
] | ||
}) | ||
) | ||
}); | ||
} | ||
|
||
function bytesToInt64(bytes) { | ||
// eslint-disable-next-line no-bitwise | ||
return bytes.subarray(0, 8).reduce((accum, b) => (accum << 8) | b, 0); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
const xdr = StellarBase.xdr; | ||
|
||
describe('building authorization entries', function () { | ||
const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE'; | ||
const kp = StellarBase.Keypair.random(); | ||
const invocation = new xdr.SorobanAuthorizedInvocation({ | ||
function: | ||
xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( | ||
new xdr.SorobanAuthorizedContractFunction({ | ||
contractAddress: new StellarBase.Address(contractId).toScAddress(), | ||
functionName: 'hello', | ||
args: [StellarBase.nativeToScVal('world!')] | ||
}) | ||
), | ||
subInvocations: [] | ||
}); | ||
|
||
it('built an mock invocation correctly', function () { | ||
invocation.toXDR(); | ||
}); | ||
|
||
it('works with keypairs', function () { | ||
const entry = StellarBase.authorizeInvocation( | ||
kp, | ||
StellarBase.Networks.FUTURENET, | ||
123, | ||
invocation | ||
); | ||
|
||
let cred = entry.credentials().address(); | ||
let args = cred.signatureArgs().map((v) => StellarBase.scValToNative(v)); | ||
|
||
expect(cred.signatureExpirationLedger()).to.equal(123); | ||
expect(args.length).to.equal(1); | ||
expect( | ||
StellarBase.StrKey.encodeEd25519PublicKey(args[0]['public_key']) | ||
).to.equal(kp.publicKey()); | ||
expect(entry.rootInvocation()).to.eql(invocation); | ||
|
||
// TODO: Validate the signature using the XDR structure. | ||
|
||
const nextEntry = StellarBase.authorizeInvocation( | ||
kp, | ||
StellarBase.Networks.FUTURENET, | ||
123, | ||
invocation | ||
); | ||
const nextCred = nextEntry.credentials().address(); | ||
|
||
expect(cred.nonce()).to.not.equal(nextCred.nonce()); | ||
}); | ||
|
||
it('works asynchronously', function (done) { | ||
StellarBase.authorizeInvocationCallback( | ||
kp.publicKey(), | ||
async (v) => kp.sign(v), | ||
StellarBase.Networks.FUTURENET, | ||
123, | ||
invocation | ||
) | ||
.then((entry) => { | ||
let cred = entry.credentials().address(); | ||
let args = cred | ||
.signatureArgs() | ||
.map((v) => StellarBase.scValToNative(v)); | ||
|
||
expect(cred.signatureExpirationLedger()).to.equal(123); | ||
expect(args.length).to.equal(1); | ||
expect( | ||
StellarBase.StrKey.encodeEd25519PublicKey(args[0]['public_key']) | ||
).to.equal(kp.publicKey()); | ||
expect(entry.rootInvocation()).to.eql(invocation); | ||
|
||
done(); | ||
}) | ||
.catch((err) => done(err)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 talked about this a bit but I wanted to ask - is there any reason to not support the async case here? I believe if we used an async signature and changed from taking a
signer
to taking aasync(input) => ...sign input and return...
then both sync/async cases would be supported. 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.
and simple keypair sync signing case can just pass in a
(input) => signer.sign(input)
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 method is cleaner because with the callback you need to pass a public key, heh, but I added
authorizeInvocationCallback
for this purpose!