Skip to content
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
merged 8 commits into from
Aug 4, 2023
Merged
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
199 changes: 199 additions & 0 deletions src/auth.js
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);

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 a async(input) => ...sign input and return... then both sync/async cases would be supported. Something like this -

export async function authorizeInvocation(
  signHash,
  networkPassphrase,
  validUntil,
  invocation
) {
  const preimage = buildAuthEnvelope(networkPassphrase, validUntil, invocation);
  const input = hash(preimage.toXDR());
  const signature = await signHash(input);
  ... rest of it ...

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)

Copy link
Contributor Author

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!

export function authorizeInvocationCallback(
  publicKey: string,
  signingMethod: (input: Buffer) => Buffer,
  networkPassphrase: string,
  validUntil: number,
  invocation: xdr.SorobanAuthorizedInvocation
): xdr.SorobanAuthorizationEntry;

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

View workflow job for this annotation

GitHub Actions / build (16)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (16)

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (16)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (16)

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (16)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (16)

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (18)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (18)

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (18)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (18)

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (18)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (18)

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (20)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (20)

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (20)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (20)

Duplicate JSDoc parameter 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (20)

Missing JSDoc parameter description for 'invocation'

Check warning on line 72 in src/auth.js

View workflow job for this annotation

GitHub Actions / build (20)

Duplicate JSDoc parameter '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);
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@ export * from './numbers';
export * from './scval';
export * from './events';
export * from './sorobandata_builder';
export * from './auth';

export default module.exports;
2 changes: 1 addition & 1 deletion src/numbers/xdr_large_int.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import xdr from '../xdr';
* @param {string} type - force a specific data type. the type choices
* are: 'i64', 'u64', 'i128', 'u128', 'i256', and 'u256' (default: the
* smallest one that fits the `value`)
* @param {number|bigint|string|ScInt|Array<number|bigint|string|ScInt>} values
* @param {number|bigint|string|XdrLargeInt|ScInt|Array<number|bigint|string|XdrLargeInt|ScInt>} values
* - a list of integer-like values interpreted in big-endian order
*/
export class XdrLargeInt {
Expand Down
78 changes: 78 additions & 0 deletions test/unit/auth_test.js
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));
});
});
8 changes: 3 additions & 5 deletions test/unit/contract_test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const NULL_ADDRESS = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM'
const NULL_ADDRESS = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM';

describe('Contract', function () {
describe('constructor', function () {
Expand All @@ -13,7 +13,7 @@ describe('Contract', function () {
expect(() => {
new StellarBase.Contract('0'.repeat(63) + '1');
}).to.throw();
})
});

it('throws on invalid ids', function () {
expect(() => {
Expand All @@ -25,9 +25,7 @@ describe('Contract', function () {
describe('address', function () {
it('returns the contract address', function () {
let contract = new StellarBase.Contract(NULL_ADDRESS);
expect(contract.address().toString()).to.equal(
NULL_ADDRESS
);
expect(contract.address().toString()).to.equal(NULL_ADDRESS);
});
});

Expand Down
29 changes: 28 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,7 @@ export function humanizeEvents(
): SorobanEvent[];

export class SorobanDataBuilder {
constructor(data?: string | xdr.SorobanTransactionData | null);
constructor(data?: string | xdr.SorobanTransactionData);

setRefundableFee(fee: IntLike): SorobanDataBuilder;
setResources(
Expand All @@ -1152,3 +1152,30 @@ export class SorobanDataBuilder {

build(): xdr.SorobanTransactionData;
}

export function authorizeInvocation(
signer: Keypair,
networkPassphrase: string,
validUntil: number,
invocation: xdr.SorobanAuthorizedInvocation
): xdr.SorobanAuthorizationEntry;

export function authorizeInvocationCallback(
publicKey: string,
signingMethod: (input: Buffer) => Buffer,
networkPassphrase: string,
validUntil: number,
invocation: xdr.SorobanAuthorizedInvocation
): xdr.SorobanAuthorizationEntry;

export function buildAuthEnvelope(
networkPassphrase: string,
validUntil: number,
invocation: xdr.SorobanAuthorizedInvocation
): xdr.HashIdPreimage;

export function buildAuthEntry(
envelope: xdr.HashIdPreimage,
signature: Buffer | Uint8Array,
publicKey: string
): xdr.SorobanAuthorizationEntry;
Loading