-
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
Changes from all commits
b7f7bc9
5eda0b4
f3908ae
4326115
bd1ebba
cc42e7f
8cca8c9
a47ff68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
Check warning on line 72 in src/auth.js GitHub Actions / build (16)
Check warning on line 72 in src/auth.js GitHub Actions / build (16)
Check warning on line 72 in src/auth.js GitHub Actions / build (16)
Check warning on line 72 in src/auth.js GitHub Actions / build (16)
Check warning on line 72 in src/auth.js GitHub Actions / build (16)
Check warning on line 72 in src/auth.js GitHub Actions / build (16)
Check warning on line 72 in src/auth.js GitHub Actions / build
Check warning on line 72 in src/auth.js GitHub Actions / build
Check warning on line 72 in src/auth.js GitHub Actions / build
Check warning on line 72 in src/auth.js GitHub Actions / build
Check warning on line 72 in src/auth.js GitHub Actions / build
Check warning on line 72 in src/auth.js GitHub Actions / build
Check warning on line 72 in src/auth.js GitHub Actions / build
Check warning on line 72 in src/auth.js GitHub Actions / build
Check warning on line 72 in src/auth.js GitHub Actions / build (18)
Check warning on line 72 in src/auth.js GitHub Actions / build (18)
Check warning on line 72 in src/auth.js GitHub Actions / build (18)
Check warning on line 72 in src/auth.js GitHub Actions / build (18)
Check warning on line 72 in src/auth.js GitHub Actions / build (18)
Check warning on line 72 in src/auth.js GitHub Actions / build (18)
Check warning on line 72 in src/auth.js GitHub Actions / build (20)
Check warning on line 72 in src/auth.js GitHub Actions / build (20)
Check warning on line 72 in src/auth.js GitHub Actions / build (20)
Check warning on line 72 in src/auth.js GitHub Actions / build (20)
Check warning on line 72 in src/auth.js GitHub Actions / build (20)
|
||
* | ||
* @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); | ||
} |
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)); | ||
}); | ||
}); |
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!