From 819f0fa4107bcdc4f2069b785879cebe329c9a45 Mon Sep 17 00:00:00 2001 From: MG Date: Tue, 30 Sep 2025 16:34:21 +0300 Subject: [PATCH 1/2] ARC-60 Upgrade --- .gitignore | 1 + ARCs/arc-0060.md | 341 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 249 insertions(+), 93 deletions(-) diff --git a/.gitignore b/.gitignore index c24da7b5c..b0a235da6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ vendor .bundle _site/* _devportal/content/*.md +.DS_Store diff --git a/ARCs/arc-0060.md b/ARCs/arc-0060.md index 515086c51..07d39e509 100644 --- a/ARCs/arc-0060.md +++ b/ARCs/arc-0060.md @@ -2,7 +2,7 @@ arc: 60 title: Algorand Wallet Arbitrary Signing API description: API function for signing data -author: Bruno Martins (@ehanoc) +author: Bruno Martins (@ehanoc), MG (@emg110) status: Draft type: Standards Track category: Interface @@ -12,134 +12,295 @@ 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. +This ARC defines a **standard wallet API for arbitrary data signing**. +It is scope-based: each scope specifies rules for inputs, validation, signing flows, and UX. +The goals are: -## Specification +- Enable safe signing of authentication data (WebAuthn-style, JWTs, OAuth messages, DIDs, etc). +- Ensure **Algorand safety**: no valid Algorand transactions, LogicSigs, or multisigs can be signed under any scope. This is ensured by-design in this spec. +- Provide a single API (`signData`) extensible with new scopes. -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 RFC-2119. +--- -> Comments like this are non-normative +## API -## Rationale +```ts +signData(request: StdSigData, metadata: Metadata): Promise +``` -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). +### StdSigData -Algorand wallets need a standard approach to byte signing to unlock self-custodial services and protect users from malicious and attack-prone signing workflows. +```ts +{ + data: string, // Base64/Base64url encoded string + signer: bytes, // Ed25519 public key + domain: string, // Domain / DID / OAuth RP / resource identifier + requestId?: string, // Optional unique request id + authenticatorData?: bytes | object, // Scope-specific + hdPath?: string // Optional BIP44 path if wallet supports derivation +} +``` + +### Metadata + +```ts +{ + scope: integer, // Scope ID + encoding: string // "base64" | "base64url" | "json" | ... +} +``` -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 +## Scopes -This ARC defines a function `signData(signingData, metadata)` for signing data. +### Scope 1: AUTH (WebAuthn/FIDO2) -`signingData` is a `StdSigData` object composed of the signing `data` that instantiates a known JSON Schema and the `signer`'s public key. +- `data`: arbitrary JSON object (auth payload). +- `authenticatorData`: **MUST** follow WebAuthn rules (rpIdHash, flags, signCount, etc). +- **Signing Flow:** + ``` + signature = EdDSA( SHA256(data) + authenticatorData ) + ``` +- Replay protection: `signCount` and `nonce`. +#### Parameters -### Signing Flow +##### `StdSigData` -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 `authenticatorData`, which will look like some random bytes. With this information, the wallet should follow the following steps: +Must be a JSON object with the following properties: -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 `authenticatorData`. - 2.1. If the hashes do not match, the wallet **MUST** return an error. -3. Append the `authenticatorData` to the resulting hash of the `data` field. -4. Sign the result +| 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. | +| `requestId` | `string` | This field is **optional**. It is used to identify the request. It **MUST** be unique for each request. | +| `authenticatorData` | `bytes` | It **MUST** include, at least, the `sha256` hash of the `domain` requesting a signature. The wallet **MUST** do an integrity check on the first 32 bytes of `authenticatorData` to match the hash. It **MAY** 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 `authenticatorData` before signing. Each `Scope` **MUST** specify if `authenticatorData` should be appended to the hash of the `data` before signing. | +| `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` -### `Scopes` +Must be a JSON object with the following properties: -Supported scopes are: +| 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. | -- `AUTH` (1): This scope is used for authentication purposes. It is used to sign data that will be used to authenticate the user to a specific domain. The `data` field **MUST** be a JSON object that represents the content to be signed. The `authenticatorData` field **MUST** include, at least, the `sha256` hash of the `domain` requesting a signature. The wallet **MUST** do an integrity check on the first 32 bytes of `authenticatorData` to match the hash. The `hdPath` field is **optional** and **MUST** be a BIP44 path in order to derive the private key to sign the `data`. The wallet **MUST** validate the path before signing. +##### `authenticatorData` +| Name | Length | Description | optional | +| ------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `rpIdHash` | 32 bytes | SHA256 hash of the domain requesting the signature. | No | +| `flags` | 1 byte | Flags (bit 0 is the least significant bit):
- 0x01: User Present (UP)
- 0 means the user is not present.
Bit 1 Reserved for future use (RFU1).
Bit 2 User Verified (UV) result.
- 1 means user is verified.
- 0 means user is not verified. Bits 3 - 5 Reserved for future use (RFU2).
Bit 6: Attested credential data included (AT).
- Indicates whether the authenticator added attested credential data.
Bit 7: Extension data included (ED).
- Indicates whether the authenticator added extension data. | yes | +| `signCount` | 4 bytes | Signature counter.
- This is a monotonically increasing counter that is incremented each time the user successfully authenticates.
- The counter is reset to 0 when the authenticator is reset.
- The counter is used to prevent replay attacks. | Yes | +| `attestedCredentialData` | variable | attested credential data (if present). See Specification | Yes | +| `extensions` | variable | extension data (if present), is a key value JSON structure that may or may not be included. See Specification for full details | Yes | -Summarized signing process for `AUTH` scope: -```plaintext -EdDSA(SHA256(data) + SHA256(authenticatorData)) -``` + This follows the FIDO WebAuthn specification for the `authenticatorData` field. The wallet **MUST** validate the `authenticatorData` field before signing. For more information on the `authenticatorData` field, please refer to the WebAuthn specification. -- **`note`**: Other scopes could be added in the future. +--- + +##### 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_AUTHENTICATED_DATA` | The `authenticatorData` field is missing. | +| `ERROR_BAD_JSON` | The `data` field is not a valid JSON object. | +| `ERROR_FAILED_DOMAIN_AUTH` | The `authenticatorData` field does not match the hash of the `domain`. | +| `ERROR_FAILED_HD_PATH` | The `hdPath` field is not a valid BIP44 path. | + +--- + +### Scope 2: JWT + +- `data`: Base64url encoded `header.payload` of a JWT. +- JWT replay/domain protection must come from claims: `aud`, `iss`, `exp`, `nbf`, `jti`. +- `authenticatorData`: Not used. **MUST** be `null` for this scope. +- Wallet **MUST** parse and display claims before signing. +- **Signing Flow:** + ``` + signature = EdDSA( SHA256(header.payload) ) + ``` +- Replay protection: Replay protection **MUST** use the jti claim (unique identifier) or a strong nonce. Wallets **MUST** reject JWTs missing both. Replay protection combines `nonce`/`state` + `timestamp`. +- Wallet **MUST** validate `exp` and `nbf` claims (to reject expired/not-yet-valid tokens). +- Wallet **MUST** reject if both `jti` and `nonce` are absent. +- `iss` and `aud` **MUST** be shown to the user before signing. #### Parameters ##### `StdSigData` -Must be a JSON object with the following properties: +| Field | Type | Description | +| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `data` | `string` | Base64url string representing the `header.payload` of a JWT. Wallet **MUST** decode and present header and claims to the user in human-readable form. | +| `signer` | `bytes` | Public key of the signer. **MUST** match an Ed25519 key in the wallet. | +| `domain` | `string` | The relying party identifier, typically the `aud` claim of the JWT. **MUST** be presented to the user. | +| `requestId` | `string` | Optional unique request identifier. | +| `authenticatorData` | `null` | Not used in JWT scope. | +| `hdPath` | `string` | Optional BIP44 path for key derivation. | -| 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. | -| `requestId` | `string` | This field is **optional**. It is used to identify the request. It **MUST** be unique for each request. | -| `authenticatorData` | `bytes` | It **MUST** include, at least, the `sha256` hash of the `domain` requesting a signature. The wallet **MUST** do an integrity check on the first 32 bytes of `authenticatorData` to match the hash. It **MAY** 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 `authenticatorData` before signing. Each `Scope` **MUST** specify if `authenticatorData` should be appended to the hash of the `data` before signing. | -| `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` + +| Field | Type | Description | +| ---------- | --------- | --------------------------------------------------------------- | +| `scope` | `integer` | Defines the purpose of the signature. It **MUST** be `2` (JWT). | +| `encoding` | `string` | **MUST** be `base64url`. | + +##### Errors + +| Error | Description | +| ------------------------- | -------------------------------------------------------------------------- | +| `ERROR_INVALID_SCOPE` | The `scope` is not `2`. | +| `ERROR_INVALID_JWT` | The `data` field is not valid base64url or does not decode to a valid JWT. | +| `ERROR_MISSING_AUD` | The JWT payload does not include an `aud` claim. | +| `ERROR_REPLAY_PROTECTION` | The JWT is expired or nonce/jti reuse detected. | + +--- + +### Scope 3: OAUTH + +- `data`: Canonical JSON of OAuth message (e.g., PKCE, DPoP proof).`data` **MUST** include a timestamp field, not just optional, so verifiers can prevent stale proofs. +- Nonce/state/PKCE challenge serve replay protection. Canonical JSON **MUST** be deterministic (sorted keys, UTF-8, no whitespace variance). +- `authenticatorData`: Not used. **MUST** be `null` for this scope. +- Wallet **MUST** display method, URI, and timestamp. +- **Signing Flow:** + ``` + signature = EdDSA( SHA256( canonicalize(data) ) ) + ``` +- Timestamp MUST be within an acceptable clock skew (e.g., ±5 minutes) + +#### Parameters + +##### `StdSigData` + +| Field | Type | Description | +| ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `data` | `string` | Canonicalized JSON string representing the OAuth signing input (method, URI, nonce, state, PKCE, etc). Wallet **MUST** display the key fields to the user. | +| `signer` | `bytes` | Public key of the signer. **MUST** match an Ed25519 key in the wallet. | +| `domain` | `string` | OAuth provider or resource domain. **MUST** be shown to the user. | +| `requestId` | `string` | Optional unique request identifier. | +| `authenticatorData` | `null` | Not used in OAUTH scope. | +| `hdPath` | `string` | Optional BIP44 path for key derivation. | ##### `metadata` -Must be a JSON object with the following properties: +| Field | Type | Description | +| ---------- | --------- | ----------------------------------------------------------------- | +| `scope` | `integer` | Defines the purpose of the signature. It **MUST** be `3` (OAUTH). | +| `encoding` | `string` | **MUST** be `json`. | -| 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 -##### `authenticatorData` +| Error | Description | +| ------------------------- | --------------------------------------------------------------- | +| `ERROR_INVALID_SCOPE` | The `scope` is not `3`. | +| `ERROR_INVALID_OAUTH` | The `data` field is not valid canonical JSON for OAuth signing. | +| `ERROR_MISSING_NONCE` | Nonce/state/PKCE missing for replay protection. | +| `ERROR_REPLAY_PROTECTION` | Nonce reuse or expired timestamp. | -| Name | Length | Description | optional | -| --- | --- | --- | --- | -| `rpIdHash` | 32 bytes | SHA256 hash of the domain requesting the signature. | No | -| `flags` | 1 byte | Flags (bit 0 is the least significant bit):
- 0x01: User Present (UP)
- 0 means the user is not present.
Bit 1 Reserved for future use (RFU1).
Bit 2 User Verified (UV) result.
- 1 means user is verified.
- 0 means user is not verified. Bits 3 - 5 Reserved for future use (RFU2).
Bit 6: Attested credential data included (AT).
- Indicates whether the authenticator added attested credential data.
Bit 7: Extension data included (ED).
- Indicates whether the authenticator added extension data. | yes | -| `signCount` | 4 bytes | Signature counter.
- This is a monotonically increasing counter that is incremented each time the user successfully authenticates.
- The counter is reset to 0 when the authenticator is reset.
- The counter is used to prevent replay attacks. | Yes | -| `attestedCredentialData` | variable | attested credential data (if present). See Specification | Yes | -| `extensions` | variable | extension data (if present), is a key value JSON structure that may or may not be included. See Specification for full details | Yes | +--- -This follows the FIDO WebAuthn specification for the `authenticatorData` field. The wallet **MUST** validate the `authenticatorData` field before signing. For more information on the `authenticatorData` field, please refer to the WebAuthn specification. +### Scope 4+: Future Scopes -##### `Errors` +- Each new scope **MUST** define: + - Accepted `data` schema. + - Rules for `authenticatorData` (mandatory/optional). + - Signing flow. + - Replay protection mechanism. +- **MUST** explicitly eliminate the possibility of data forms that could represent Algorand transactions or logicsigs. -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_AUTHENTICATED_DATA` | The `authenticatorData` field is missing. | -| `ERROR_BAD_JSON` | The `data` field is not a valid JSON object. | -| `ERROR_FAILED_DOMAIN_AUTH` | The `authenticatorData` field does not match the hash of the `domain`. | -| `ERROR_FAILED_HD_PATH` | The `hdPath` field is not a valid BIP44 path. | +## General Signing Flow (Revised) -## Backwards Compatibility +1. Validate `scope`. +2. Validate `data` according to scope rules. +3. The wallet **MUST** enforce replay protection according to the rules of the selected scope. If replay protection fails, the wallet **MUST** reject the request. +4. If scope requires `authenticatorData`, validate according to scope definition. +5. Apply **scope-specific signing flow**. +6. Return signature + metadata. -N / A +--- -## Reference Implementation +## Example Requests -Available in the `assets/arc-0060` folder. +### AUTH -### Sample Use cases +```json +{ + "data": "eyJzdGF0ZW1lbnQiOiAiU2lnbiB0byBsb2dpbiJ9", + "signer": "", + "domain": "arc60.io", + "authenticatorData": "" +} +``` -#### Generic AUTH +### JWT -```ts - const authData: Uint8Array = new Uint8Array(createHash('sha256').update("arc60.io").digest()) +```json +{ + "data": "eyJhbGciOiJFZERTQSJ9.eyJhdWQiOiAiYXJjNjAuaW8iLCAibm9uY2UiOiAiYWJjMTIzIn0", + "signer": "", + "domain": "arc60.io" +} +``` - 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" - } +### OAUTH - const signResponse = await arc60wallet.signData(authRequest, { scope: ScopeType.AUTH, encoding: 'base64' }) +```json +{ + "data": "{ \"method\": \"POST\", \"uri\": \"https://arc60.io/token\", \"nonce\": \"xyz123\" }", + "signer": "", + "domain": "arc60.io" +} ``` -#### CAIP-122 +--- + +## Security Considerations + +- Wallets **SHOULD** sanitize inputs and enforce scope rules. +- Wallets are free to make their own UX choices, but they **MUST** 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. +- wallets **MUST** show to the user the data that is being signed in a human-readable format, as well as the authenticatorData and how it was calculated, so that the hash can be verified by the user when signing with ledger for example. + +--- + +## Examples + +### Generic Example (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"), + authenticatorData: authData, + hdPath: "m/44'/60'/0'/0/0", +}; + +const signResponse = await arc60wallet.signData(authRequest, { + scope: ScopeType.AUTH, + encoding: "base64", +}); +``` + +### CAIP-122 Example (AUTH) + +[CAIP-122](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-122.md) is a chain-agnostic framework for blockchain-based authentication and authorization on off-chain services. SIWX (sign in with....) , or [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361?utm_source=chatgpt.com) is a specific implementation of CAIP-122 for different blockchains (e.g. SIWE). The generic AUTH scope of this ARC can be used to sign CAIP-122 messages and therefore be used to provide SIWA (sign in with Algorand) feature. ```ts const caip122Request: CAIP122 = { @@ -154,10 +315,10 @@ Available in the `assets/arc-0060` folder. ... } - // Disply message title according EIP-4361 + // Display message title according to EIP-4361 const msgTitle: string = `Sign this message to authenticate to ${caip122Request.domain} with account ${caip122Request.account_address}` - // Display message body according EIP-4361 + // Display message body according to EIP-4361 const msgBodyPlaceHolders: string = `URI: ${caip122Request.uri}\n` + `Chain ID: ${caip122Request.chain_id}\n` + `Type: ${caip122Request.type}\n` + `Nonce: ${caip122Request.nonce}\n` @@ -167,20 +328,20 @@ Available in the `assets/arc-0060` folder. + `Issued At: ${caip122Request["issued-at"]}\n` + `Resources: ${(caip122Request.resources ?? []).join(' , \n')}\n` - // Display message according EIP-4361 + // Display message according to EIP-4361 const msg: string = `${msgTitle}\n\n${msgBodyPlaceHolders}` console.log(msg) - // authenticationData - const authenticationData: Uint8Array = new Uint8Array(createHash('sha256').update(caip122Request.domain).digest()) + // authenticatorData + const authenticatorData: Uint8Array = new Uint8Array(createHash('sha256').update(caip122Request.domain).digest()) const signData: StdSigData = { data: Buffer.from(JSON.stringify(caip122Request)).toString('base64'), signer: publicKey, - domain: caip122Request.domain, // should be same as origin / authenticationData + domain: caip122Request.domain, // should be same as origin / authenticatorData // random unique id, to help RP / Client match requests requestId: Buffer.from(randomBytes(32)).toString('base64'), - authenticationData: authenticationData + authenticatorData: authenticatorData } const signResponse = await arc60wallet.signData(signData, { scope: ScopeType.AUTH, encoding: 'base64' }) @@ -189,12 +350,6 @@ Available in the `assets/arc-0060` folder. // reply ``` -## Security Considerations - - Wallets are free to make their own UX choices, but they **SHOULD** 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. - - Additionally, wallets **MUST** show to the user the data that is being signed in a human-readable format, as well as the authenticatorData and how it was calculated, so that the hash can be verified by the user when signing with ledger for example. - ## Copyright Copyright and related rights waived via CCO. From ba0dacaba6bbea94d221409997ca90b4e08d93ea Mon Sep 17 00:00:00 2001 From: MG Date: Tue, 30 Sep 2025 17:00:35 +0300 Subject: [PATCH 2/2] ARC-60 Update: Fixes, Add JWT and OAUTH scopes --- ARCs/arc-0060.md | 59 ++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/ARCs/arc-0060.md b/ARCs/arc-0060.md index 07d39e509..b4e10782f 100644 --- a/ARCs/arc-0060.md +++ b/ARCs/arc-0060.md @@ -12,7 +12,7 @@ requires: 1 ## Abstract -This ARC defines a **standard wallet API for arbitrary data signing**. +This ARC defines a **standard wallet API for arbitrary data signing**. It is designed to be a simple and flexible standard that can be used in a wide variety of applications. It is scope-based: each scope specifies rules for inputs, validation, signing flows, and UX. The goals are: @@ -20,9 +20,24 @@ The goals are: - Enable safe signing of authentication data (WebAuthn-style, JWTs, OAuth messages, DIDs, etc). - Ensure **Algorand safety**: no valid Algorand transactions, LogicSigs, or multisigs can be signed under any scope. This is ensured by-design in this spec. - Provide a single API (`signData`) extensible with new scopes. +- Wallets can choose which scopes to implement. --- +## 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 RFC-2119. + +> 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. + ## API ```ts @@ -122,18 +137,18 @@ These are the possible errors that the wallet **MUST** handle: ### Scope 2: JWT -- `data`: Base64url encoded `header.payload` of a JWT. +- `data`: Base64url encoded `header.payload` of a JWT. `authenticatorData` is not used and **MUST** be `null` for this scope. - JWT replay/domain protection must come from claims: `aud`, `iss`, `exp`, `nbf`, `jti`. -- `authenticatorData`: Not used. **MUST** be `null` for this scope. - Wallet **MUST** parse and display claims before signing. +- Replay protection: Replay protection **MUST** use the jti claim (unique identifier) or a strong nonce. Wallets **MUST** reject JWTs missing both. Replay protection combines `nonce`/`state` + `timestamp`. +- Wallet **MUST** reject JWTs missing both `jti` and `nonce`. +- Wallet **MUST** reject JWTs where `exp` has passed or `nbf` is in the future. +- Wallet **MUST** reject JWTs with `alg` not equal to `EdDSA` (Ed25519). +- Wallet **MUST** display both `aud` and `iss` claims to the user. - **Signing Flow:** - ``` + ```ts signature = EdDSA( SHA256(header.payload) ) ``` -- Replay protection: Replay protection **MUST** use the jti claim (unique identifier) or a strong nonce. Wallets **MUST** reject JWTs missing both. Replay protection combines `nonce`/`state` + `timestamp`. -- Wallet **MUST** validate `exp` and `nbf` claims (to reject expired/not-yet-valid tokens). -- Wallet **MUST** reject if both `jti` and `nonce` are absent. -- `iss` and `aud` **MUST** be shown to the user before signing. #### Parameters @@ -145,7 +160,6 @@ These are the possible errors that the wallet **MUST** handle: | `signer` | `bytes` | Public key of the signer. **MUST** match an Ed25519 key in the wallet. | | `domain` | `string` | The relying party identifier, typically the `aud` claim of the JWT. **MUST** be presented to the user. | | `requestId` | `string` | Optional unique request identifier. | -| `authenticatorData` | `null` | Not used in JWT scope. | | `hdPath` | `string` | Optional BIP44 path for key derivation. | ##### `metadata` @@ -159,7 +173,7 @@ These are the possible errors that the wallet **MUST** handle: | Error | Description | | ------------------------- | -------------------------------------------------------------------------- | -| `ERROR_INVALID_SCOPE` | The `scope` is not `2`. | +| `ERROR_INVALID_SCOPE` | The `scope` is not valid. | | `ERROR_INVALID_JWT` | The `data` field is not valid base64url or does not decode to a valid JWT. | | `ERROR_MISSING_AUD` | The JWT payload does not include an `aud` claim. | | `ERROR_REPLAY_PROTECTION` | The JWT is expired or nonce/jti reuse detected. | @@ -168,15 +182,17 @@ These are the possible errors that the wallet **MUST** handle: ### Scope 3: OAUTH -- `data`: Canonical JSON of OAuth message (e.g., PKCE, DPoP proof).`data` **MUST** include a timestamp field, not just optional, so verifiers can prevent stale proofs. -- Nonce/state/PKCE challenge serve replay protection. Canonical JSON **MUST** be deterministic (sorted keys, UTF-8, no whitespace variance). -- `authenticatorData`: Not used. **MUST** be `null` for this scope. -- Wallet **MUST** display method, URI, and timestamp. -- **Signing Flow:** - ``` - signature = EdDSA( SHA256( canonicalize(data) ) ) - ``` -- Timestamp MUST be within an acceptable clock skew (e.g., ±5 minutes) +- `data`: Canonical JSON of OAuth message (e.g., PKCE, DPoP proof). `data` **MUST** include a timestamp field so verifiers can prevent stale proofs. `authenticatorData` is not used and **MUST** be `null` for this scope. +- `nonce`/`state`/PKCE challenge serve replay protection. +- Canonical JSON **MUST** be deterministic: sorted keys, UTF-8 encoding, no extraneous whitespace. +- Wallet **MUST** display method, URI, and timestamp. +- Timestamp **MUST** be within an acceptable clock skew (e.g., ±5 minutes). +- Wallet **MUST** reject requests where `timestamp` is outside the acceptable skew. +- Wallet **MUST** reject requests missing both `nonce/state` and `timestamp`. +- **Signing Flow:** +```ts +signature = EdDSA( SHA256(canonicalize(data)) ) +``` #### Parameters @@ -187,8 +203,7 @@ These are the possible errors that the wallet **MUST** handle: | `data` | `string` | Canonicalized JSON string representing the OAuth signing input (method, URI, nonce, state, PKCE, etc). Wallet **MUST** display the key fields to the user. | | `signer` | `bytes` | Public key of the signer. **MUST** match an Ed25519 key in the wallet. | | `domain` | `string` | OAuth provider or resource domain. **MUST** be shown to the user. | -| `requestId` | `string` | Optional unique request identifier. | -| `authenticatorData` | `null` | Not used in OAUTH scope. | +| `requestId` | `string` | Optional unique request scope. | | `hdPath` | `string` | Optional BIP44 path for key derivation. | ##### `metadata` @@ -202,7 +217,7 @@ These are the possible errors that the wallet **MUST** handle: | Error | Description | | ------------------------- | --------------------------------------------------------------- | -| `ERROR_INVALID_SCOPE` | The `scope` is not `3`. | +| `ERROR_INVALID_SCOPE` | The `scope` is not valid. | | `ERROR_INVALID_OAUTH` | The `data` field is not valid canonical JSON for OAuth signing. | | `ERROR_MISSING_NONCE` | Nonce/state/PKCE missing for replay protection. | | `ERROR_REPLAY_PROTECTION` | Nonce reuse or expired timestamp. |