-
Notifications
You must be signed in to change notification settings - Fork 118
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
ARC60 (new) - arb data signing AUTH, random, fido2, caip122 #313
base: main
Are you sure you want to change the base?
Changes from all commits
c204990
39e8e8f
e42b6dc
0680788
f4dbe9d
e4d5eed
1bfcfde
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,233 @@ | ||
--- | ||
arc: 60 | ||
title: Algorand Wallet Arbitrary Signing API | ||
description: API function for signing data | ||
author: Bruno Martins (@ehanoc) | ||
status: Draft | ||
type: Standards Track | ||
category: Interface | ||
created: 2024-10-27 | ||
requires: 1 | ||
--- | ||
|
||
## Abstract | ||
|
||
This ARC proposes a standard for arbitrary data signing. It is designed to be a simple and flexible standard that can be used in a wide variety of applications. | ||
|
||
|
||
## 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 | ||
|
||
## Rationale | ||
|
||
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 to be signed 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. | ||
|
||
### Overview | ||
|
||
This ARC defines a function `signData(signingData, metadata)` for signing data. | ||
|
||
`signingData` is a `StdSigData` object composed of the signing `data` that instantiates a known JSON Schema and the `signer`'s public key. | ||
|
||
|
||
### Signing Flow | ||
|
||
When connected to a specific `domain` (i.e app or other identifier), the wallet will receive a request to sign some `data` along side some `authenticatedData`, which will look like some random bytes. With this information, the wallet should follow the following steps: | ||
|
||
1. Hash the `data` field with `sha256`. | ||
2. Knowing to what `domain` we are connected to, hash such value with `sha256` and compare it with the first 32 bytes of `authenticatedData`. | ||
2.1. If the hashes do not match, the wallet **MUST** return an error. | ||
3. Append the `authenticatedData` to the resulting hash of the `data` field. | ||
4. Sign the result | ||
|
||
|
||
### `Scopes` | ||
|
||
Supported scopes are: | ||
|
||
- `AUTH` (1): This scope is used for authentication purposes. It is used to sign data that will be used to authenticate the user. The `data` field **MUST** be a JSON object with the following fields: | ||
- `type`: the type of operation (e.g., "SIWA", “webauthn.create”, “webauthn.get”, etc) | ||
- `origin`: The origin / domain of the request or the URL of the relying party (RP) that initiated the authentication. | ||
- `challenge`: 32 bytes of entropy. | ||
|
||
Summarized signing process for `AUTH` scope: | ||
```plaintext | ||
EdDSA(SHA256(data) + SHA256(authenticatedData)) | ||
``` | ||
|
||
|
||
#### `StdSigData` | ||
|
||
Must be a JSON object with the following properties: | ||
|
||
| Field | Type | Description | | ||
| --- | --- | --- | | ||
| `data` | `string` | string representing the content to be signed for the specific `Scope`. This can be an encoded JSON object or any other data. It **MUST** be presented to the user in a human-readable format. | | ||
| `signer` | `bytes` | public key of the signer. This can the public related to an Algorand address or any other Ed25519 public key. | | ||
| `domain` | `string` | This is the domain requesting the signature. It can be a URL, a DID, or any other identifier. It **MUST** be presented to the user to inform them about the context of the signature. | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Must the domain also be matched against WalletConnect, for example? This means, if a user is connected to dapp There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup :) |
||
| `requestId` | `string` | It is used to identify the request. It **MUST** be unique for each request. | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How can uniqueness be enforced? Would a wallet have to keep track of past There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not in reality, but tracking uuids for instance is pretty straightforward. |
||
| `authenticatedData` | `bytes` | It **MUST** include, at least, the `sha256` hash of the `domain` requesting a signture. The wallet **MUST** do an integrity check on the first 32 bytes of `authenticatedData` to match the hash. It **COULD** also include signature counters, network flags or any other unique data to prevent replay attacks or to trick user to sign unrelated data to the scope. The wallet **SHOULD** validate every field in `authenticatedData` before signing. Each `Scope` **MUST** specify if `authenticatedData` should be appended to the hash of the `data` before signing. | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spec states that wallets SHOULD validate every field in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes; can be scope specific. The most important are the first 32 bytes that must be the SHA256(domain); also calculated by the wallet; ensures that the domain you are "talking" to is the same one you expect to be "connected" to and sending you data to sign. All the remaining bytes, are context specific and depending on the scope validations could vary |
||
| `hdPath` | `string` | This field is **optional**. It is required if the wallet supports BIP39 / BIP32 / BIP44. This field **MUST** be a BIP44 path in order to derive the private key to sign the `data`. The wallet **MUST** validate the path before signing. | | ||
|
||
#### `metadata` | ||
|
||
Must be a JSON object with the following properties: | ||
|
||
| Field | Type | Description | | ||
| --- | --- | --- | | ||
| `scope` | `integer` | Defines the purpose of the signature. It **MUST** be one of the following values: `1` (AUTH) | | ||
| `encoding` | `string` | Defines the encoding of the `data` field. `base64` is the recommended encoding. | | ||
|
||
#### `Errors` | ||
|
||
These are the possible errors that the wallet **MUST** handle: | ||
|
||
| Error | Description | | ||
| --- | --- | | ||
| `ERROR_INVALID_SCOPE` | The `scope` is not valid. | | ||
| `ERROR_FAILED_DECODING` | The `data` field could not be decoded. | | ||
| `ERROR_INVALID_SIGNER` | Unable to find in the wallet the public key related to the signer. | | ||
| `ERROR_MISSING_DOMAIN` | The `domain` field is missing. | | ||
| `ERROR_MISSING_DOMAIN` | The `authenticatedData` field is missing. | | ||
| `ERROR_BAD_JSON` | The `data` field is not a valid JSON object. | | ||
| `ERROR_FAILED_DOMAIN_AUTH` | The `authenticatedData` field does not match the hash of the `domain`. | | ||
| `ERROR_FAILED_HD_PATH` | The `hdPath` field is not a valid BIP44 path. | | ||
|
||
## Backwards Compatibility | ||
|
||
N / A | ||
|
||
## Reference Implementation | ||
|
||
Available in the `assets/arc-0060` folder. | ||
|
||
### Sample Use cases | ||
|
||
#### Generic AUTH | ||
|
||
```ts | ||
const authData: Uint8Array = new Uint8Array(createHash('sha256').update("arc60.io").digest()) | ||
|
||
const authRequest: StdSigData = { | ||
data: Buffer.from("{[jsonfields....]}").toString('base64'), | ||
signer: publicKey, | ||
domain: "arc60.io", | ||
requestId: Buffer.from(randomBytes(32)).toString('base64'), | ||
authenticationData: authData, | ||
hdPath: "m/44'/60'/0'/0/0" | ||
} | ||
|
||
const signResponse = await arc60wallet.signData(authRequest, { scope: ScopeType.AUTH, encoding: 'base64' }) | ||
``` | ||
|
||
#### CAIP-122 | ||
|
||
```ts | ||
const caip122Request: CAIP122 = { | ||
domain: "arc60.io", | ||
chain_id: "283", | ||
account_address: ... | ||
type: "ed25519", | ||
statement: "We are requesting you to sign this message to authenticate to arc60.io", | ||
uri: "https://arc60.io", | ||
version: "1", | ||
nonce: Buffer.from(randomBytes(32)).toString, | ||
... | ||
} | ||
|
||
// Disply message title according EIP-4361 | ||
const msgTitle: string = `Sign this message to authenticate to ${caip122Request.domain} with account ${caip122Request.account_address}` | ||
|
||
// Display message body according EIP-4361 | ||
const msgBodyPlaceHolders: string = `URI: ${caip122Request.uri}\n` + `Chain ID: ${caip122Request.chain_id}\n` | ||
+ `Type: ${caip122Request.type}\n` | ||
+ `Nonce: ${caip122Request.nonce}\n` | ||
+ `Statement: ${caip122Request.statement}\n` | ||
+ `Expiration Time: ${caip122Request["expiration-time"]}\n` | ||
+ `Not Before: ${caip122Request["not-before"]}\n` | ||
+ `Issued At: ${caip122Request["issued-at"]}\n` | ||
+ `Resources: ${(caip122Request.resources ?? []).join(' , \n')}\n` | ||
|
||
// Display message according EIP-4361 | ||
const msg: string = `${msgTitle}\n\n${msgBodyPlaceHolders}` | ||
console.log(msg) | ||
|
||
// authenticationData | ||
const authenticationData: Uint8Array = new Uint8Array(createHash('sha256').update(caip122Request.domain).digest()) | ||
|
||
const signData: StdSigData = { | ||
data: Buffer.from(JSON.stringify(caip122Request)).toString('base64'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great point. Yes, it should be deterministic JSON There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated @k13n |
||
signer: publicKey, | ||
domain: caip122Request.domain, // should be same as origin / authenticationData | ||
// random unique id, to help RP / Client match requests | ||
requestId: Buffer.from(randomBytes(32)).toString('base64'), | ||
authenticationData: authenticationData | ||
} | ||
|
||
const signResponse = await arc60wallet.signData(signData, { scope: ScopeType.AUTH, encoding: 'base64' }) | ||
expect(signResponse).toBeDefined() | ||
|
||
// reply | ||
``` | ||
|
||
#### FIDO2 / Webauthn | ||
|
||
```ts | ||
|
||
// FIDO2 request | ||
const fido2Request: FIDO2ClientData = { | ||
origin: "https://webauthn.io", | ||
rpId: rpId, | ||
challenge: "g8OebU4sWOCGljYnKXw4WUFNDszbeWfBJJKwmrTHuvc" | ||
} | ||
|
||
const rpHash: Buffer = createHash('sha256').update(rpId).digest() | ||
|
||
// Set the flag for behavior | ||
const up = true | ||
const uv = true | ||
const be = true | ||
const bs = true | ||
var flags: number = 0 | ||
if (up) { | ||
flags = flags | 0x01 | ||
} | ||
if (uv) { | ||
flags = flags | 0x04 | ||
} | ||
if (be) { | ||
flags = flags | 0x08 | ||
} | ||
if (bs) { | ||
flags = flags | 0x10 | ||
} | ||
|
||
const authData: Uint8Array = new Uint8Array(Buffer.concat([rpHash, Buffer.from([flags]), Buffer.from([0, 0, 0, 0])])) | ||
|
||
const signData: StdSigData = { | ||
data: Buffer.from(JSON.stringify(fido2Request)).toString('base64'), | ||
signer: publicKey, | ||
domain: "webauthn.io", // should be same as origin / authenticationData | ||
// random unique id, to help RP / Client match requests | ||
requestId: Buffer.from(randomBytes(32)).toString('base64'), | ||
authenticationData: authData | ||
} | ||
|
||
const signResponse = await arc60wallet.signData(signData, { scope: ScopeType.AUTH, encoding: 'base64' }) | ||
|
||
``` | ||
|
||
|
||
## Security Considerations | ||
|
||
Wallets are free to make their own UX choices, but they **SHOULD** to show the user the purpose (i.e. `scope`) of the signature, the domain that is requesting the signature, and the data that is being signed. This is to prevent users from signing data that they do not understand. | ||
|
||
## Copyright | ||
|
||
Copyright and related rights waived via <a href="https://creativecommons.org/publicdomain/zero/1.0/">CCO</a>. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# 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/ | ||
dist/ | ||
coverage/ | ||
|
||
# Optional npm cache directory | ||
.npm | ||
|
||
# Optional eslint cache | ||
.eslintcache | ||
|
||
# Misc files and directories | ||
.DS_Store | ||
.fleet | ||
.idea | ||
*.local | ||
yarn.lock | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
!.vscode/settings.json.example | ||
.idea | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? | ||
.vscode-test |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# ARC-60 Reference Implementation | ||
|
||
## Overview | ||
|
||
This is a reference implementation of the ARC-60 specification. It is written in TypeScript and uses the Jest testing framework. | ||
The test suite shows the different use cases of the ARC-60 specification. | ||
|
||
## Instructions | ||
|
||
```bash | ||
$ yarn | ||
$ yarn test | ||
``` | ||
|
||
## Sample Output | ||
|
||
```bash | ||
PASS ./arc60wallet.api.spec.ts | ||
ARC60 TEST SUITE | ||
rawSign | ||
✓ (OK) should sign data correctly (22 ms) | ||
✓ (FAILS) should throw error for shorter incorrect length seed (1 ms) | ||
✓ (FAILS) should throw error for longer incorrect length seed (1 ms) | ||
getPublicKey | ||
✓ (OK) should return the correct public key | ||
✓ (FAILS) should throw error for shorter incorrect length seed | ||
✓ (FAILS) should throw error for longer incorrect length seed (1 ms) | ||
SCOPE == INVALID | ||
✓ (FAILS) Tries to sign with invalid scope (18 ms) | ||
AUTH sign request | ||
✓ (OK) Signing AUTH requests (3 ms) | ||
✓ (FAILS) Tries to sign with bad json (2 ms) | ||
✓ (FAILS) Tries to sign with bad json schema (1 ms) | ||
✓ (FAILS) Is missing domain (1 ms) | ||
✓ (FAILS) Is missing authenticationData (1 ms) | ||
Invalid or Unkown Signer | ||
✓ (FAILS) Tries to sign with bad signer (1 ms) | ||
Unknown Encoding | ||
✓ (FAILS) Tries to sign with unknown encoding (1 ms) | ||
|
||
-------------------|---------|----------|---------|---------|------------------- | ||
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | ||
-------------------|---------|----------|---------|---------|------------------- | ||
All files | 100 | 100 | 100 | 100 | | ||
...0wallet.api.ts | 100 | 100 | 100 | 100 | | ||
-------------------|---------|----------|---------|---------|------------------- | ||
Test Suites: 1 passed, 1 total | ||
Tests: 14 passed, 14 total | ||
Snapshots: 0 total | ||
Time: 2.568 s, estimated 3 s | ||
Ran all test suites. | ||
Done in 3.08s. | ||
|
||
``` |
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.
In previous iterations of ARC60 the requirement was stated that it must be possible to sign exactly the input data (e.g., a 32 byte hash). Here the input data is hashed before it is signed, which means it's not possible to sign exactly the input data. Is that requirement no longer necessary? Because that'd be great and would simplify things a lot compared to the past.
One concern that I have is that the resulting hash might still by chance (or by design from a bad actor) contain a
Program
prefix, which makes the hash a valid logic signature. In the worst case, a logicsig could be crafted that takes control of the user's account.D13 explained that a future version of this ARC will explicitly prevent
Program
prefixes, which is great.Another option is to add a fixed prefix like so:
EdDSA('arc60' + SHA256(data) + SHA256(authenticatedData))
. Since we already modify the user's input before signing, I don't see a problem with adding a prefix.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.
"prefix" are a really bad practice in my opinion; prevents any subject to validate signature crafted in algorand wallets / dapps and generically verify them without conforming to our own custom way of doing things.
We can add a check to prevent "Program"; sure. But the chances of you finding a hash with valid 7 bytes (Program) + the remainder of the program are extremely low.
Still, i can add a check to reject "Program"