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 helper functions to sign authorization entries. #537

Merged
merged 10 commits into from
Sep 19, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps.
## 0.41.0-beta.4
* Add support for Soroban Preview 11. ([#530](https://github.com/stellar/java-stellar-sdk/pull/530))
* Bump dependencies & Remove unnecessary dependencies. ([#523](https://github.com/stellar/java-stellar-sdk/pull/523))
* Add helper functions to sign authorization entries. ([#537](https://github.com/stellar/java-stellar-sdk/pull/537))
* No longer provide a shadow jar that has relocated third-party dependencies, but instead default to providing a thin jar.
We also offer an [uber jar](https://docs.gradle.org/current/userguide/working_with_files.html#sec:creating_uber_jar_example) and javadoc jar.
You can import the jar you need in one of the following three ways: ([#528](https://github.com/stellar/java-stellar-sdk/issues/528))
Expand Down
303 changes: 303 additions & 0 deletions src/main/java/org/stellar/sdk/Auth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
package org.stellar.sdk;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.LinkedHashMap;
import org.stellar.sdk.scval.Scv;
import org.stellar.sdk.xdr.EnvelopeType;
import org.stellar.sdk.xdr.Hash;
import org.stellar.sdk.xdr.HashIDPreimage;
import org.stellar.sdk.xdr.Int64;
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.xdr.SorobanAddressCredentials;
import org.stellar.sdk.xdr.SorobanAuthorizationEntry;
import org.stellar.sdk.xdr.SorobanAuthorizedInvocation;
import org.stellar.sdk.xdr.SorobanCredentials;
import org.stellar.sdk.xdr.SorobanCredentialsType;
import org.stellar.sdk.xdr.Uint32;
import org.stellar.sdk.xdr.XdrUnsignedInteger;

/** This class contains helper methods to sign {@link SorobanAuthorizationEntry}. */
public class Auth {
/**
* Actually authorizes an existing authorization entry using the given the credentials and
Copy link
Contributor

@sreuland sreuland Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consideration in a later pr, could remove this duplicated docs by using @see instead:

/**
 * @see Auth#authorizeEntry(String, KeyPair, Long, Network) authorizeEntry
 */

and maybe re-evaluate the overloaded signatures of authorizeEntry and see if that can be narrowed?

* expiration details, returning a signed copy.
*
* <p>This "fills out" the authorization entry with a signature, indicating to the {@link
* InvokeHostFunctionOperation} it's attached to that:
*
* <ul>
* <li>a particular identity (i.e. signing {@link KeyPair} or {@link Signer})
* <li>approving the execution of an invocation tree (i.e. a simulation-acquired {@link
* SorobanAuthorizedInvocation} or otherwise built)
* <li>on a particular network (uniquely identified by its passphrase, see {@link Network})
* <li>until a particular ledger sequence is reached.
* </ul>
*
* @param entry a base64 encoded unsigned Soroban authorization entry
* @param signer a {@link KeyPair} which should correspond to the address in the `entry`
* @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this
* authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired)
* @param network the network is incorprated into the signature
* @return a signed Soroban authorization entry
*/
public static SorobanAuthorizationEntry authorizeEntry(
String entry, KeyPair signer, Long validUntilLedgerSeq, Network network) {
SorobanAuthorizationEntry entryXdr;
try {
entryXdr = SorobanAuthorizationEntry.fromXdrBase64(entry);
} catch (IOException e) {
throw new IllegalArgumentException("Unable to convert entry to SorobanAuthorizationEntry", e);
}
return authorizeEntry(entryXdr, signer, validUntilLedgerSeq, network);
}

/**
* Actually authorizes an existing authorization entry using the given the credentials and
* expiration details, returning a signed copy.
*
* <p>This "fills out" the authorization entry with a signature, indicating to the {@link
* InvokeHostFunctionOperation} it's attached to that:
*
* <ul>
* <li>a particular identity (i.e. signing {@link KeyPair} or {@link Signer})
* <li>approving the execution of an invocation tree (i.e. a simulation-acquired {@link
* SorobanAuthorizedInvocation} or otherwise built)
* <li>on a particular network (uniquely identified by its passphrase, see {@link Network})
* <li>until a particular ledger sequence is reached.
* </ul>
*
* @param entry a base64 encoded unsigned Soroban authorization entry
* @param signer a {@link KeyPair} which should correspond to the address in the `entry`
* @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this
* authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired)
* @param network the network is incorprated into the signature
* @return a signed Soroban authorization entry
*/
public static SorobanAuthorizationEntry authorizeEntry(
SorobanAuthorizationEntry entry, KeyPair signer, Long validUntilLedgerSeq, Network network) {
Signer entrySigner =
preimage -> {
byte[] data;
try {
data = preimage.toXdrByteArray();
} catch (IOException e) {
throw new IllegalArgumentException("Unable to convert preimage to bytes", e);
}
byte[] payload = Util.hash(data);
return signer.sign(payload);
};

return authorizeEntry(entry, entrySigner, validUntilLedgerSeq, network);
}

/**
* Actually authorizes an existing authorization entry using the given the credentials and
* expiration details, returning a signed copy.
*
* <p>This "fills out" the authorization entry with a signature, indicating to the {@link
* InvokeHostFunctionOperation} it's attached to that:
*
* <ul>
* <li>a particular identity (i.e. signing {@link KeyPair} or {@link Signer})
* <li>approving the execution of an invocation tree (i.e. a simulation-acquired {@link
* SorobanAuthorizedInvocation} or otherwise built)
* <li>on a particular network (uniquely identified by its passphrase, see {@link Network})
* <li>until a particular ledger sequence is reached.
* </ul>
*
* @param entry a base64 encoded unsigned Soroban authorization entry
* @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the
* signature of the hash of the raw payload bytes, see {@link Signer}
* @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this
* authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired)
* @param network the network is incorprated into the signature
* @return a signed Soroban authorization entry
*/
public static SorobanAuthorizationEntry authorizeEntry(
String entry, Signer signer, Long validUntilLedgerSeq, Network network) {
SorobanAuthorizationEntry entryXdr;
try {
entryXdr = SorobanAuthorizationEntry.fromXdrBase64(entry);
} catch (IOException e) {
throw new IllegalArgumentException("Unable to convert entry to SorobanAuthorizationEntry", e);
}
return authorizeEntry(entryXdr, signer, validUntilLedgerSeq, network);
}

/**
* Actually authorizes an existing authorization entry using the given the credentials and
* expiration details, returning a signed copy.
*
* <p>This "fills out" the authorization entry with a signature, indicating to the {@link
* InvokeHostFunctionOperation} it's attached to that:
*
* <ul>
* <li>a particular identity (i.e. signing {@link KeyPair} or {@link Signer})
* <li>approving the execution of an invocation tree (i.e. a simulation-acquired {@link
* SorobanAuthorizedInvocation} or otherwise built)
* <li>on a particular network (uniquely identified by its passphrase, see {@link Network})
* <li>until a particular ledger sequence is reached.
* </ul>
*
* @param entry an unsigned Soroban authorization entry
* @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the
* signature of the hash of the raw payload bytes, see {@link Signer}
* @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this
* authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired)
* @param network the network is incorprated into the signature
* @return a signed Soroban authorization entry
*/
public static SorobanAuthorizationEntry authorizeEntry(
SorobanAuthorizationEntry entry, Signer signer, Long validUntilLedgerSeq, Network network) {
SorobanAuthorizationEntry clone;
try {
clone = SorobanAuthorizationEntry.fromXdrByteArray(entry.toXdrByteArray());
} catch (IOException e) {
throw new IllegalArgumentException("Unable to clone SorobanAuthorizationEntry", e);
}

if (clone.getCredentials().getDiscriminant()
!= SorobanCredentialsType.SOROBAN_CREDENTIALS_ADDRESS) {
return clone;
}

SorobanAddressCredentials addressCredentials = clone.getCredentials().getAddress();
addressCredentials.setSignatureExpirationLedger(
new Uint32(new XdrUnsignedInteger(validUntilLedgerSeq)));

HashIDPreimage preimage =
new HashIDPreimage.Builder()
.discriminant(EnvelopeType.ENVELOPE_TYPE_SOROBAN_AUTHORIZATION)
.sorobanAuthorization(
new HashIDPreimage.HashIDPreimageSorobanAuthorization.Builder()
.networkID(new Hash(network.getNetworkId()))
.nonce(addressCredentials.getNonce())
.invocation(clone.getRootInvocation())
.signatureExpirationLedger(addressCredentials.getSignatureExpirationLedger())
.build())
.build();
byte[] signature = signer.sign(preimage);
byte[] publicKey = Address.fromSCAddress(addressCredentials.getAddress()).getBytes();

byte[] data;
try {
data = preimage.toXdrByteArray();
} catch (IOException e) {
throw new IllegalArgumentException("Unable to convert preimage to bytes", e);
}
byte[] payload = Util.hash(data);
if (!KeyPair.fromPublicKey(publicKey).verify(payload, signature)) {
throw new IllegalArgumentException("signature does not match payload");
}

// This structure is defined here:
// https://soroban.stellar.org/docs/fundamentals-and-concepts/invoking-contracts-with-transactions#stellar-account-signatures
SCVal sigScVal =
Scv.toMap(
new LinkedHashMap<SCVal, SCVal>() {
{
put(Scv.toSymbol("public_key"), Scv.toBytes(publicKey));
put(Scv.toSymbol("signature"), Scv.toBytes(signature));
}
});
addressCredentials.setSignature(Scv.toVec(Collections.singleton(sigScVal)));
return clone;
}

/**
* This builds an entry from scratch, allowing you to express authorization as a function of:
*
* <ul>
* <li>a particular identity (i.e. signing {@link KeyPair} or {@link Signer})
* <li>approving the execution of an invocation tree (i.e. a simulation-acquired {@link
* SorobanAuthorizedInvocation} or otherwise built)
* <li>on a particular network (uniquely identified by its passphrase, see {@link Network})
* <li>until a particular ledger sequence is reached.
* </ul>
*
* <p>This is in contrast to {@link Auth#authorizeEntry}, which signs an existing entry "in
* place".
*
* @param signer a {@link KeyPair} used to sign the entry
* @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this
* authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired)
* @param invocation invocation the invocation tree that we're authorizing (likely, this comes
* from transaction simulation)
* @param network the network is incorprated into the signature
* @return a signed Soroban authorization entry
*/
public static SorobanAuthorizationEntry authorizeInvocation(
KeyPair signer,
Long validUntilLedgerSeq,
SorobanAuthorizedInvocation invocation,
Network network) {
Signer entrySigner =
preimage -> {
try {
byte[] payload = Util.hash(preimage.toXdrByteArray());
return signer.sign(payload);
} catch (IOException e) {
throw new RuntimeException(e);
}
};
return authorizeInvocation(
entrySigner, signer.getAccountId(), validUntilLedgerSeq, invocation, network);
}

/**
* This builds an entry from scratch, allowing you to express authorization as a function of:
*
* <ul>
* <li>a particular identity (i.e. signing {@link KeyPair} or {@link Signer})
* <li>approving the execution of an invocation tree (i.e. a simulation-acquired {@link
* SorobanAuthorizedInvocation} or otherwise built)
* <li>on a particular network (uniquely identified by its passphrase, see {@link Network})
* <li>until a particular ledger sequence is reached.
* </ul>
*
* <p>This is in contrast to {@link Auth#authorizeEntry}, which signs an existing entry "in
* place".
*
* @param signer A function which takes a payload (a {@link HashIDPreimage}) and returns the
* signature of the hash of the raw payload bytes, see {@link Signer}
* @param publicKey the public identity of the signer
* @param validUntilLedgerSeq the (exclusive) future ledger sequence number until which this
* authorization entry should be valid (if `currentLedgerSeq==validUntil`, this is expired)
* @param invocation invocation the invocation tree that we're authorizing (likely, this comes
* from transaction simulation)
* @param network the network is incorprated into the signature
* @return a signed Soroban authorization entry
*/
public static SorobanAuthorizationEntry authorizeInvocation(
Signer signer,
String publicKey,
Long validUntilLedgerSeq,
SorobanAuthorizedInvocation invocation,
Network network) {
long nonce = new SecureRandom().nextLong();
SorobanAuthorizationEntry entry =
new SorobanAuthorizationEntry.Builder()
.credentials(
new SorobanCredentials.Builder()
.discriminant(SorobanCredentialsType.SOROBAN_CREDENTIALS_ADDRESS)
.address(
new SorobanAddressCredentials.Builder()
.address(new Address(publicKey).toSCAddress())
.nonce(new Int64(nonce))
.signatureExpirationLedger(
new Uint32(new XdrUnsignedInteger(validUntilLedgerSeq)))
.signature(Scv.toVoid())
.build())
.build())
.rootInvocation(invocation)
.build();
return authorizeEntry(entry, signer, validUntilLedgerSeq, network);
}

/** An interface for signing a {@link HashIDPreimage} to produce a signature. */
public interface Signer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice usage of functional interface!

byte[] sign(HashIDPreimage preimage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Singular;
import lombok.Value;
import lombok.experimental.SuperBuilder;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -46,9 +46,7 @@ public class InvokeHostFunctionOperation extends Operation {
@NonNull HostFunction hostFunction;

/** The authorizations required to execute the host function */
@Singular("auth")
@NonNull
Collection<SorobanAuthorizationEntry> auth;
@NonNull @Builder.Default List<SorobanAuthorizationEntry> auth = new ArrayList<>();

/**
* Constructs a new InvokeHostFunctionOperation object from the XDR representation of the {@link
Expand Down
35 changes: 34 additions & 1 deletion src/main/java/org/stellar/sdk/SorobanServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,40 @@ public SimulateTransactionResponse simulateTransaction(Transaction transaction)
*/
public Transaction prepareTransaction(Transaction transaction)
throws IOException, SorobanRpcErrorResponse, PrepareTransactionException {
SimulateTransactionResponse simulateTransactionResponse = this.simulateTransaction(transaction);
SimulateTransactionResponse simulateTransactionResponse = simulateTransaction(transaction);
return prepareTransaction(transaction, simulateTransactionResponse);
}

/**
* Prepare the transaction using the simulateTransaction obtained by the user in advance, apply
* the simulateTransaction results to a new copy of the transaction which is then returned.
* Setting the ledger footprint and authorization, so the resulting transaction is ready for
* signing and sending.
*
* <p>The returned transaction will also have an updated fee that is the sum of fee set on
* incoming transaction with the contract resource fees estimated from simulation. It is advisable
* to check the fee on returned transaction and validate or take appropriate measures for
* interaction with user to confirm it is acceptable.
*
* @param transaction The transaction to prepare. It should include exactly one operation, which
* must be one of {@link InvokeHostFunctionOperation}, {@link
* BumpFootprintExpirationOperation}, or {@link RestoreFootprintOperation}. Any provided
* footprint will be ignored. You can use {@link Transaction#isSorobanTransaction()} to check
* if a transaction is a Soroban transaction. Any provided footprint will be overwritten.
* However, if your operation has existing auth entries, they will be preferred over ALL auth
* entries from the simulation. In other words, if you include auth entries, you don't care
* about the auth returned from the simulation. Other fields (footprint, etc.) will be filled
* as normal.
* @param simulateTransactionResponse The {@link SimulateTransactionResponse} to use for preparing
* the transaction.
* @return Returns a copy of the {@link Transaction}, with the expected authorizations (in the
* case of invocation) and ledger footprint added. The transaction fee will also automatically
* be padded with the contract's minimum resource fees discovered from the simulation.
* @throws PrepareTransactionException If preparing the transaction fails.
*/
public Transaction prepareTransaction(
Transaction transaction, SimulateTransactionResponse simulateTransactionResponse)
throws PrepareTransactionException {
if (simulateTransactionResponse.getError() != null) {
throw new PrepareTransactionException(
"simulation transaction failed, the response contains error information.",
Expand Down
Loading
Loading