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

ARC-60 Algorand Wallet Structured Data Signing API #284

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
70dd6d3
byte signing arc
deanstef Mar 5, 2024
e17febd
rm ds_store
deanstef Mar 5, 2024
92ded6a
ARC Validator CR
deanstef Mar 6, 2024
f33ae41
ARC Validator CR 2
deanstef Mar 6, 2024
67754a8
bump bulk sigs
deanstef Mar 6, 2024
df31e97
Fix linting
SudoWeezy Mar 11, 2024
7447a0d
arc60 review
deanstef Mar 12, 2024
8d8cef3
simplified schema + nits
deanstef Mar 16, 2024
2472440
leftovers
deanstef Mar 16, 2024
90a15d7
fix ref impl + nits
deanstef Mar 21, 2024
e92a668
check tag in bytes
deanstef Mar 21, 2024
dc9b230
nit
deanstef Mar 21, 2024
045de9e
removed canonicalized stuff
deanstef Apr 8, 2024
a8de5d0
typo
deanstef Apr 8, 2024
cafda81
remove unused dep
deanstef Apr 9, 2024
4a7173d
bump message
deanstef Apr 9, 2024
c08b995
- clarify `data` object in Overview
deanstef Apr 25, 2024
a33817e
nit
deanstef Apr 25, 2024
b244ed6
add AUTH scope and example + update ref impl + clarity nits
deanstef Apr 25, 2024
88c3c32
HD wallets support
deanstef Apr 26, 2024
17c149e
nit
deanstef Apr 26, 2024
282aac4
fix auth-schema.json ref
deanstef Apr 26, 2024
0285805
typo 1
deanstef May 17, 2024
f992d9e
replace StdSigData with StdSignData
deanstef May 17, 2024
43a7178
overview: more expressive example + json type fix
deanstef May 17, 2024
20ecf74
ref. impl. align encoding with default b64
deanstef May 17, 2024
d379f17
clarify encoding object
deanstef May 17, 2024
19720ff
more checks with 'Program' prefix
deanstef May 17, 2024
5c4184e
fix typo
deanstef May 17, 2024
4bad240
gen statement on msg
deanstef May 17, 2024
83a4b79
ref display message section
deanstef May 17, 2024
88923d3
reject not supported encoding
deanstef May 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
330 changes: 330 additions & 0 deletions ARCs/arc-0060.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
---
arc: 60
title: Algorand Wallet Structured Data Signing API
description: API function fot signing structured data
author: Stefano De Angelis (@deanstef)
discussions-to: https://github.com/algorandfoundation/ARCs/issues/284
status: Draft
type: Standards Track
category: Interface
created: 2024-02-28
requires: 1
---

# Algorand Wallet Structured Data Signing API

> This ARC is inspired by [ARC-1](./arc-0001.md).

## Abstract

ARC-1 defines a standard for signing Algorand transactions that are represented as structured objects. This proposal extends the signing to generic byte arrays encoded with a standardized structure.

[ARC-60](./arc-0060.md) defines an API for wallets to sign structured data that are not Algorand transactions.

## Motivation

Signing data is a common and critical operation. Users may need to sign data for multiple reasons (e.g. delegate signatures, DIDs, signing documents, authentication).

Algorand wallets need a standard approach to byte signing to unlock self-custodial services and protect users from malicious and attack-prone signing workflows.

This ARC provides a standard API for bytes signing. The API encodes byte arrays into well structured JSON schemas together with additional metadata. It requires wallets to validate the signing inputs, notify users about what they are signing and warn them in case of dangerous signing requests.

## Specification

The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in <a href="https://www.ietf.org/rfc/rfc2119.txt">RFC-2119</a>.

> Comments like this are non-normative

### Overview

> This section is non-normative

Signatures of bytes are processed with the `signData(data, metadata, signer)` function.
deanstef marked this conversation as resolved.
Show resolved Hide resolved

`data` is a string representing the structured data being signed. It is the representation of a JSON Schema, provided with the `metadata`.

`metadata` is a `StdSignMetadata` object that describes the signature scope and the type of data being signed, including the encoding and the JSON schema.

`signer` is a byte array representing the public key to use for the signing operation.

### Interfaces

> Interfaces are defined in TypeScript. All the objects that are defined are valid JSON objects.

ARC-60 uses interchangeably the terms "throw an error" and "reject a promise with an error".

#### **Interface `SignDataFunction`**

A wallet function `signData` is defined by the interface:

```tsx
export type SignDataFunction = {
data: StdData,
metadata: StdSignMetadata,
signer: Ed25519Pk,
}
=> Promise<(SignedDataStr | null)>;
Copy link
Contributor

Choose a reason for hiding this comment

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

In which cases can signData return null? Further below, it is defined that if signData fails, it should throw an error, not return null. So shouldn't the function either return SigendDataStr or throw an error?

Copy link
Author

Choose a reason for hiding this comment

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

Yes you are right. This is a leftover from a previous design with bulk signatures.

Copy link
Author

Choose a reason for hiding this comment

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

Actually - in the reference implemnetation, I am returning null if the user does not confimr the signature: https://github.com/deanstef/ARCs/blob/a8de5d08f3c558bedeb8c1addbacc25362eda7b5/assets/arc-0060/wallet.ts#L79C4-L79C5

```

- `data` is a `StdData` object representing the structured data being signed (defined below).
- `metadata` is a `StdSignMetadata` objects (defined below) that provide additional information on the data being signed, such as the `data` encoding and the JSON schema.
- `signer` is a `Ed25519Pk` object (defined below) that represents the signer public key.

The `signData` function returns a `SignedDataStr` object or, in case of error, rejects the promise with an error object `SignDataError`.

#### Interface `StdData`

The `StdData` object is a string that comply with the `metadata` JSON schema. It **MUST** be the byte representation of a canonicalized JSON object, following the <a href="https://www.rfc-editor.org/rfc/rfc8785">RFC 8785</a>.

```tsx
export type StdData = string;
```

The `StdData` must be validated with respect the JSON schema provided with `metadata` (defined below).

#### Interface `Ed25519Pk`

An `Ed25519Pk` object is a 32-byte public key, point of the ed25519 elliptic curve. The key **MUST NOT** be transformed into an Algorand address.

```tsx
export type Ed25519Pk = Uint8Array;
```

> The wallet **MAY** want to operate with standard Algorand address directly. The transformation from a generic key to an Algorand address is left to the implementation. See <a href="https://developer.algorand.org/docs/get-details/accounts/#transformation-public-key-to-algorand-address">Public Key to Algorand Address</a> section of the developer portal.

#### Interface `SignedDataStr`

`SignedDataStr` is the produced 64-byte array, ed25519 digital signature, of the signed data.
Copy link
Contributor

Choose a reason for hiding this comment

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

According to this definition, SigendDataStr is purely the signature (64 bytes long). In the reference implementation, nacl.sign is used which returns the signature and the original message.

If SignedDataStr represents purely the signature, wouldn't something like Signature be a more appropriate name for this type?

Copy link
Author

Choose a reason for hiding this comment

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

Good catch. Here b244ed6 I updated both the naming and the reference implementation. I am using nacl.sign.detached now.


```tsx
export type SignedDataStr = Uint8Array;
deanstef marked this conversation as resolved.
Show resolved Hide resolved
```

#### Enum `ScopeType`

The `ScopeType` enumerates constant strings with the scope of a signing action.

This ARC introduces two scope types.

| ScopeType | Description |
| --- | --- |
| MSGSIG | Signature of a simple message. This is the most generic scope. |
deanstef marked this conversation as resolved.
Show resolved Hide resolved
| LSIG | Signature of an Algorand program for delegation. |
| ... | ... |

Any extension of this ARC **SHOULD** adopt the `ScopeType` above, or introduce a new scope.

#### Interface `StdSignMetadata`

A `StdSignMetadata` object specifies metadata of a `StdSignData` that is being signed.

```tsx
export interface StdSignMetadata {
/**
* The scope value of the signing data request.
*/
scope: ScopeType;

/**
* Canonical representation of the JSON schema for the signing data.
*/
schema: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the schema (which is a JSON object) represented as a canonicalized JSON string?

Copy link
Author

Choose a reason for hiding this comment

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

In a very first version of ARC60 I was using canonicalized versions of JSON objects to produce signatures of the entire objects. In the new version this is not needed anymore. We can get rid of the canonicalized stuff indeed.

Copy link
Author

Choose a reason for hiding this comment

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


/**
* Optional message explaining the reason for the signature.
*/
message?: string;

/**
* Optional encoding used to represent the signing data.
*/
encoding?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

What are possible encodings that should be supported?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to clarifying this

Copy link
Author

Choose a reason for hiding this comment

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

It can be any encoding. If the wallet does not support it, it will just reject the call. I make it explicit here 88923d3

}
```

If the `encoding` is not specified, the `StdData` object should be UTF-8 encoded following the <a href="https://www.rfc-editor.org/rfc/rfc8785">RFC 8785</a>.

##### Simple Message Data JSON Schema

The JSON schema for simple message signing. The `StdData` **MUST** be an object of this schema.
deanstef marked this conversation as resolved.
Show resolved Hide resolved

> The signing data JSON schema is inspired by the schema proposed with <a href="https://eips.ethereum.org/EIPS/eip-712">EIP-712: Typed structured data hashing and signing proposal</a>.

```json
{
"type": "object",
"properties": {
"ARC60Domain": {
"type": "string",
"description": "The ARC-60 domain separator"
},
"bytes": {
"type": "string",
"description": "Byte string being signed. Used with ScopeType==MSGSIG."
},
"required": ["ARC60Domain", "bytes"]
}
deanstef marked this conversation as resolved.
Show resolved Hide resolved
```

- The `ARC60Domain` object indicates the domain separator to be used for signing. It **SHOULD** be set to `"arc60"` for simple bytes. Algorand transaction domain separators `TX` and `TG` **MUST** be forbidden.
- The `bytes` object is the byte array of data being signed. It is a simple string in case of `ScopeType==MSGSIG`, a more complex schema **SHOULD** be used otherwise. It cannot be a valid Algorand transaction. The value of `bytes` **MUST NOT** be prepended with a known domain separator `TX` or `TG`.

> Algorand domain separators can be found in the <a href="https://github.com/algorandfoundation/specs/blob/master/dev/crypto.md#domain-separation">Algorand specs</a> and in the <a href="https://github.com/algorand/go-algorand/blob/master/protocol/hash.go#L21">Go type HashID</a>.

For example, a valid object that takes as bytes an extra property field is:

```json
{
"ARC60Domain" : "arc60",
"bytes" : "ARC-60 is awesome"
}
```
deanstef marked this conversation as resolved.
Show resolved Hide resolved

#### Error interface `SignDataError`

The `SignDataError` object extends the `SignTxnsError` defined in [ARC-1](./arc-0001.md).

```ts
export interface SignDataError extends Error {
code: number;
data?: any;
failingSignData: (StdData | null);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

In which case could failingSignData be null?

Copy link
Author

@deanstef deanstef Apr 8, 2024

Choose a reason for hiding this comment

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

If the error was caused by a missing StdData object.

```

`SignDataError` uses the same error codes of `SignTxnsError` as well as the following codes:

| Status Code | Name | Description |
| --- | --- | --- |
| 4600 | Invalid scope | The scope of the signing action is not recognized by the wallet |
| 4601 | Invalid schema | The schema does not comply with ARC-60 requirements |
| 4602 | Invalid encoding | The canonical JSON cannot be decoded with the given encoding |

### Semantic Requirements

The call `signData(data, metadata, signer)` **MUST** either return the signed data `ret` or reject the call throwing an error `err`.
Copy link
Contributor

Choose a reason for hiding this comment

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

According to the definition of SignDataFunction above, it seems a null return value is also possible (which I don't think is required as I mentioned earlier).


> Following [ARC-1](./arc-0001.md) terminology, in this ARC the term **Rejecting** means throwing an error with `err.code=4300`.

Upon calling `signData(data, metadata, signer)`:

- the `data`, `metadata`, and `signer` **MUST NOT** be `null`, otherwise the wallet **MUST** reject the call.
- `data` **MUST** be validated with respect to the JSON schema `metadata.schema`. If the validation fails, the call **MUST** be rejected with a `4300` error.
Copy link
Contributor

Choose a reason for hiding this comment

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

According to the table above an invalid schema is a 4601 error code, or is this a different schema?

Copy link
Author

Choose a reason for hiding this comment

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

Error code 4601 means that the JSON schema does not comply with ARC-60 (the required fields ARC60Domain and bytes are missing).

In this case, we are failing because data is not a valid object according to the schema metadata.schema.

- if the encoding `metadata.encoding` is present, it **MUST** be used to encode `data`.
- the wallet **MUST** prepare the signing data as `signData=(<data.bytes>)`.
- if the value of `ARC6Domain` is not empty than it must used as `prefix`: `signData=(<prefix> || <data.bytes>)`.
deanstef marked this conversation as resolved.
Show resolved Hide resolved
- the wallet **MUST** ask users for signing confirmation. It **MUST** display the `metadata.message` if present, and the `signData` being signed:
- if the user approves, `signData` **MUST** be signed with `signer` and `ret` **MUST** be set to the corresponding `SignedDataStr`.
- if the user rejects, the call **MUST** fail with error code `4001`.

Note that if `signData` cannot be signed for any reason, the wallet **MUST** throw an error, such that

- `err.message` **SHOULD** indicate the reason of the error (e.g. specify that `data` is not a valid JSON object according to `metadata.schema`)
- `err.failingSignData` **SHOULD** return the `StdData` object that caused the error, otherwise `null`.

#### Semantic of `data`

- it **MUST** be a valid `StdData` object, otherwise the wallet **MUST** reject.
- the encoding **MUST** be equal to the value specified with `metadata.encoding` if any, otherwise it **MUST** be UTF-8.
- if `data` cannot be decoded into a canonicalized JSON object, the wallet **MUST** throw a `4602` error.
- if the decoded `data` does not comply with the JSON schema in `metadata`, the wallet **MUST** reject.
- the `ARC60Domain` filed must be validated against forbidden values:
- the wallet **MUST** reject if `ARC60Domain="TX"`.
- the wallet **MUST** reject if `ARC60Domain="TG"`.
- the wallet **MUST** reject if `metadata.scope=MSGSIG` and `ARC60Domain` field is not set to `"arc60"` or `""`.
- the `bytes` must be a valid byte string:
- the wallet **MUST** reject if `bytes` is prepended with a forbidden domain separator `TX` or `TG`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this validation here independent of the ARC60Domain validation? Because if ARC60Domain is set to arc60 (or anything other than "", "TX", or "TG"), wouldn't it be fine if bytes started with TX or TG? Because the wallet would have to concatenate the value of ARC60Domain and bytes and hence no malicious payload could be signed?

Copy link
Author

Choose a reason for hiding this comment

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

Setting ARC60Domain to "" would make the concatenation not effective. I suggest to always validate bytes against malicious payloads (prepended with TX or TG) to avoid problems.


#### Semantic of `StdSignMetadata`

- `scope`:
- it **MUST** be a valid `ScopeType` string, otherwise the wallet **MUST** throw a `4600` error.

- `schema`:
- it **MUST** be a canonical JSON schema with the properties `ARC60Domain` and `bytes`, otherwise the wallet **MUST** throw a `4601` error.
- the wallet **MAY** accept JSON schema with `bytes` with different structures (e.g. a JSON object with additional parameters). In that case, the wallet **MUST** verify that the `data.bytes` reflects the structure of the schema.

- `message`:
- If specified, the message **SHOULD** be displayed to the user in plain text. The wallet **SHOULD** display the message `metadata.message`.

- `encoding`:
- if specified, it **MUST** be used to encode the `data`.
- the wallet **MUST** throw a `4602` error if the decoding fails with the given encoding value.

#### Semantic of `signer`

- it **MUST** be a valid `Ed25519Pk` object, otherwise the wallet **MUST** reject.
- the wallet **MUST** reject if `signer` is unknown.

#### General Validation

Every input of the `signData(data, metadata, signer)` must be validated.

The validation:

- **SHALL NOT** rely on TypeScript typing as this can be bypassed. Types **MUST** be manually verified.
- **SHALL NOT** assume that the provided `data` complies with the respective `metadata.schema`. The schema **MUST** be manually verified and all the required parameters **MUST** be checked.
- **SHALL NOT** assume that signatures are computed with the Algorand SDK `signBytes` function. There is no indirect validation on the passed `data` and `metadata` objects on that function. In general, the validation **SHALL NOT** rely on underlying SDKs to validate the inputs of an ARC-60 signing operation.

#### Display warnings

The wallet **MUST** display a warning message when the signing request (`ScopeType`) is related to a known Algorand action like `LSIG`, i.e. signing a logic signature for delegation.

## Rationale

This API was designed to enable a secure and structured signing generic data with Algorand wallets. The API:

- Is generic to _pure_ ed25519 key pairs and do not require keys manipulation
- Is easy to extend with custom JSON schema to support future signing use cases (e.g. authentication)
- Is secure by design preventing malicious applications to trick users signing malicious messages or in worst cases approve malicious transactions.

This API was not designed to sign Algorand transactions or group of transactions (see ARC-1 instead).

## Backwards Compatibility
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we should even mention ARC-47 in this ARC here. I'm inclined to remove it for the sake of simplicity and to keep this ARC as short and concise as possible. If there's demand for signing lsigs with ARC-60, somebody can propose a new ARC, specify the scope, and define the schema, etc.


This ARC is not backward compatible with [ARC-47](./arc-0047.md) Logic Signature Template.

ARC-47 introduces the method `algo_templatedLsig` that takes a `LogicSignatureDescription` object and returns the signature of a logic signature program for delegation.

Al alternative using the ARC-60 `signData()` is given below, where:

- `data` **MUST** specify:
- the `LogicSignatureDescription` object as detailed in ARC-47
- the `values` of the templated variables and the `hash` of the expected program
- the `ARC60Domain` must be set to the domain separator `"Program"`, as specified in the <a href="https://github.com/algorandfoundation/specs/blob/master/dev/crypto.md#domain-separation">Algorand specs</a>.
- `metadata` **MUST** specify:
- an extended ARC-60 compatible JSON schema
- `ScopeType` set to `LSIG`.

The extended schema can be found in the `../assets/arc-0060` folder at the file [lsig-schema.json](../assets/arc-0060/lsig-schema.json).

### Semantics for signing a Logic Signature

- if the `metadata.scope` is set to `LSIG`:
- the `ARC60Domain` value **MUST** be equal to the byte array corresponding to the prefix `"Program"`.
- the JSON schema **MUST** define `data.bytes` as an array of objects, such as the `LogicSignatureDescription`, `values`, and `hash`.
- the wallet **MUST** follow the ARC-47 specification: compile the program from the received template and check the integrity.
- the computed `signData` beign signed **MUST** be equal to the compiled program prepended with the `"Program"` domain separator, otherwise the wallet **MUST** fail.

## Test Cases
Copy link
Contributor

Choose a reason for hiding this comment

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

Once the ARC is stable and about to be finalized, it'd be nice to add some test cases. We could specify a keypair, a message (according to the AUTH schema) and show the resulting signature as a test case.

In another test case we could have a keypair derived from a hd-wallet.


N/A

## Reference Implementation

A reference implementation is available in the `../assets/arc-0060` folder.

- [simple-schema.json](../assets/arc-0060/simple-schema.json) provides a simple ARC-60 JSON schema
- [wallet.ts](../assets/arc-0060/wallet.ts) contains a simple TypeScript script showing how wallets can implement the `SignDataFunction` interface

## Security Considerations

Users signing bytes can be tricked to sign malicious messages being exposed to man-in-the-middle attacks.

Users must be aware of what they are signing and for what purpose. The wallet **MUST** always show the message being signed along with the scope of the signing action and the message (if any). The requested signer **MUST** be displayed as well.

> For example:"You are about to sign the bytes `ARC60 is awesome` with scope `MSGSIG` and message `Sign arbitrary bytes` with the key `1Cz6hgjfllYajnodkJ2hCi+GiYF6ndMHMstH9tJOqlg=`".

## Copyright

Copyright and related rights waived via <a href="https://creativecommons.org/publicdomain/zero/1.0/">CCO</a>.
38 changes: 38 additions & 0 deletions assets/arc-0060/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Compiled javascript
*.js

# Dependency directories
node_modules/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Misc files and directories
.DS_Store
.fleet
.idea
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json.example
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vscode-test