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..b4e10782f 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,8 +12,17 @@ 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 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: + +- 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 @@ -29,37 +38,47 @@ Algorand wallets need a standard approach to byte signing to unlock self-custodi 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. +## API +```ts +signData(request: StdSigData, metadata: Metadata): Promise +``` -### 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 `authenticatorData`, 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 `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 +### StdSigData +```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 +} +``` -### `Scopes` +### Metadata -Supported scopes are: +```ts +{ + scope: integer, // Scope ID + encoding: string // "base64" | "base64url" | "json" | ... +} +``` -- `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. +--- +## Scopes -Summarized signing process for `AUTH` scope: -```plaintext -EdDSA(SHA256(data) + SHA256(authenticatorData)) -``` +### Scope 1: AUTH (WebAuthn/FIDO2) -- **`note`**: Other scopes could be added in the future. +- `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 @@ -67,79 +86,236 @@ EdDSA(SHA256(data) + SHA256(authenticatorData)) 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. | -| `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. | +| 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` 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. | +| 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. | ##### `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 | +| 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. + 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. -##### `Errors` +--- + +##### 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. | +| 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. `authenticatorData` is not used and **MUST** be `null` for this scope. +- JWT replay/domain protection must come from claims: `aud`, `iss`, `exp`, `nbf`, `jti`. +- 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) ) + ``` + +#### Parameters + +##### `StdSigData` + +| 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. | +| `hdPath` | `string` | Optional BIP44 path for key derivation. | -## Backwards Compatibility +##### `metadata` -N / A +| Field | Type | Description | +| ---------- | --------- | --------------------------------------------------------------- | +| `scope` | `integer` | Defines the purpose of the signature. It **MUST** be `2` (JWT). | +| `encoding` | `string` | **MUST** be `base64url`. | -## Reference Implementation +##### Errors -Available in the `assets/arc-0060` folder. +| Error | Description | +| ------------------------- | -------------------------------------------------------------------------- | +| `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. | -### Sample Use cases +--- -#### Generic AUTH +### Scope 3: OAUTH +- `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 - const authData: Uint8Array = new Uint8Array(createHash('sha256').update("arc60.io").digest()) +signature = EdDSA( SHA256(canonicalize(data)) ) +``` - 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" - } +#### 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 scope. | +| `hdPath` | `string` | Optional BIP44 path for key derivation. | + +##### `metadata` - const signResponse = await arc60wallet.signData(authRequest, { scope: ScopeType.AUTH, encoding: 'base64' }) +| Field | Type | Description | +| ---------- | --------- | ----------------------------------------------------------------- | +| `scope` | `integer` | Defines the purpose of the signature. It **MUST** be `3` (OAUTH). | +| `encoding` | `string` | **MUST** be `json`. | + +##### Errors + +| Error | Description | +| ------------------------- | --------------------------------------------------------------- | +| `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. | + +--- + +### Scope 4+: Future Scopes + +- 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. + +--- + +## General Signing Flow (Revised) + +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. + +--- + +## Example Requests + +### AUTH + +```json +{ + "data": "eyJzdGF0ZW1lbnQiOiAiU2lnbiB0byBsb2dpbiJ9", + "signer": "", + "domain": "arc60.io", + "authenticatorData": "" +} +``` + +### JWT + +```json +{ + "data": "eyJhbGciOiJFZERTQSJ9.eyJhdWQiOiAiYXJjNjAuaW8iLCAibm9uY2UiOiAiYWJjMTIzIn0", + "signer": "", + "domain": "arc60.io" +} +``` + +### OAUTH + +```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 +330,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 +343,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 +365,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.