|
| 1 | +# Protected Cards |
| 2 | + |
| 3 | +The protected cards feature allows securing an Uphold card by associating a cryptographic key pair to it, |
| 4 | +so that a signature with the secret key of that pair will be required for performing transactions with it |
| 5 | +(except deposits into the card, which remain possible with unsigned requests). |
| 6 | + |
| 7 | +The instructions below describe in detail how to make use of this feature. |
| 8 | + |
| 9 | +## Generate a Key Pair |
| 10 | + |
| 11 | +> Create an EdDSA cryptographic key pair, e.g. using the [tweetnacl](https://www.npmjs.com/package/tweetnacl) package: |
| 12 | +
|
| 13 | +```js |
| 14 | +const nacl = require('tweetnacl'); |
| 15 | +const keyPair = nacl.sign.keyPair(); // Generate an Ed25519 key pair |
| 16 | +const publicKey = Buffer.from(keyPair.publicKey).toString('hex'); |
| 17 | +const secretKey = Buffer.from(keyPair.secretKey).toString('hex'); |
| 18 | +``` |
| 19 | + |
| 20 | +The public-private key pair used for signing transactions of protected cards must be generated by you, |
| 21 | +and it must be of the [EdDSA](https://en.wikipedia.org/wiki/EdDSA) type (specifically, `Ed25519`). |
| 22 | + |
| 23 | +This step should be performed on your server-side, and the private key must be stored securely. |
| 24 | +Never expose or transmit the private key to Uphold, or any other third party outside your control. |
| 25 | + |
| 26 | +## Create a Protected Card |
| 27 | + |
| 28 | +> Construct the body of the request to create a card: |
| 29 | +
|
| 30 | +```js |
| 31 | +const data = { |
| 32 | + currency: 'USD', |
| 33 | + label: 'My Protected Card', |
| 34 | + publicKey: publicKey |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +> Create a SHA-256 (or SHA-512) hash of the request body, and store it as a base64-encoded digest: |
| 39 | +
|
| 40 | +```js |
| 41 | +const crypto = require('crypto'); |
| 42 | +const digest = crypto.createHash('sha256').update(JSON.stringify(data)).digest('base64'); |
| 43 | +``` |
| 44 | + |
| 45 | +> Create the signature, e.g. using the [http-request-signature](https://www.npmjs.com/package/http-request-signature) package: |
| 46 | +
|
| 47 | +```js |
| 48 | +const { sign } = require('http-request-signature'); |
| 49 | +const signature = sign({ |
| 50 | + headers: { |
| 51 | + digest: `SHA-256=${digest}` // This must match the `Digest` header |
| 52 | + }, // that will be sent in the request. |
| 53 | + keyId: 'primary', // We require the `keyId` to be "primary". |
| 54 | + secretKey |
| 55 | +}, { algorithm: 'ed25519' }); // We only support the `ed25519` algorithm. |
| 56 | +``` |
| 57 | + |
| 58 | +> Submit the request including the `Digest` and the `Signature` headers, as well as the data used to generate them: |
| 59 | +
|
| 60 | +```bash |
| 61 | +$ curl 'https://api.uphold.com/v0/me/cards' \ |
| 62 | + -H 'Accept: application/json' \ |
| 63 | + -H 'Authorization: Bearer <token>' \ |
| 64 | + -H 'Digest: SHA-256=<digest>' \ |
| 65 | + -H 'Signature: <signature>' \ |
| 66 | + -H 'Content-Type: application/json' \ |
| 67 | + -d '{ "currency": "USD", "label": "My Protected Card", "publicKey": "<publicKey>" }' |
| 68 | +``` |
| 69 | + |
| 70 | +> The response should be a [card object](https://uphold.com/en/developer/api/documentation/#card-object) |
| 71 | +> (sample output truncated for conciseness): |
| 72 | +
|
| 73 | +```json |
| 74 | +{ |
| 75 | + "available": "0.00", |
| 76 | + "balance": "0.00", |
| 77 | + "currency": "USD", |
| 78 | + "id": "71064207-b557-4808-ac33-e4eb86d78a01", |
| 79 | + "label": "My Protected Card" |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +Once a key pair has been generated, a protected card can be created |
| 84 | +by adding the public key in the data of a request for [creating a card](https://uphold.com/en/developer/api/documentation/#create-card). |
| 85 | + |
| 86 | +As additional safeguards, two headers must be included in this request: |
| 87 | +a `Digest` (consisting of a SHA-256 hash of the body of the request) to guard against transmission errors in the request data; and |
| 88 | +a `Signature` (consisting of the same digest payload, but encrypted with the private key, and formatted to be compliant with the draft Internet Standard |
| 89 | +"[Signing HTTP Messages](https://tools.ietf.org/html/draft-cavage-http-signatures-12)"), |
| 90 | +which validates that the public key included in the data does match the private key used for encryption. |
| 91 | + |
| 92 | +<aside class="notice"> |
| 93 | + Keep in mind that the data sent in the request body must be an <b>exact string match</b> |
| 94 | + to the input used to generate the <code>Digest</code> and the <code>Signature</code> headers. |
| 95 | + In the example shown here, it must equal the output of <code>JSON.stringify()</code>, which is fed into <code>crypto.createHash()</code>. |
| 96 | +</aside> |
| 97 | + |
| 98 | +## Create Signed Transactions |
| 99 | + |
| 100 | +> Construct the body of the request to create a transaction from a protected card: |
| 101 | +
|
| 102 | +```js |
| 103 | +const data = { |
| 104 | + denomination: { |
| 105 | + amount: '10', |
| 106 | + currency: USD' |
| 107 | + }, |
| 108 | + destination: '<address>' |
| 109 | +} |
| 110 | +``` |
| 111 | +
|
| 112 | +> Generate the digest and signature of the request data: |
| 113 | +
|
| 114 | +```js |
| 115 | +const crypto = require('crypto'); |
| 116 | +const digest = crypto.createHash('256').update(JSON.stringify(data)).digest('base64'); |
| 117 | +
|
| 118 | +const { sign } = require('http-request-signature'); |
| 119 | +const signature = sign({ |
| 120 | + headers: { |
| 121 | + digest: `SHA-256=${digest}` |
| 122 | + }, |
| 123 | + keyId: 'primary', |
| 124 | + secretKey |
| 125 | +}, { algorithm: 'ed25519' }); |
| 126 | +``` |
| 127 | +
|
| 128 | +> Submit a request for creating a signed transaction, using the id of the protected card in the URL parameters: |
| 129 | +
|
| 130 | +```bash |
| 131 | +$ curl 'https://api.uphold.com/v0/me/cards/<id>/transactions?commit=true' \ |
| 132 | + -H 'Accept: application/json' \ |
| 133 | + -H 'Authorization: Bearer <token>' \ |
| 134 | + -H 'Digest: SHA-256=<digest>' \ |
| 135 | + -H 'Signature: <signature>' \ |
| 136 | + -H 'Content-Type: application/json' \ |
| 137 | + -d <data> |
| 138 | +``` |
| 139 | +
|
| 140 | +> Uphold's server verifies that the transaction's signature is correct and proceeds with committing the transaction |
| 141 | +> (sample output truncated for conciseness): |
| 142 | +
|
| 143 | +```json |
| 144 | +{ |
| 145 | + "createdAt": "2017-06-26T17:17:57.532Z", |
| 146 | + "denomination": { |
| 147 | + "pair": "USDUSD", |
| 148 | + "rate": "1.00", |
| 149 | + "amount": "1.00", |
| 150 | + "currency": "USD" |
| 151 | + }, |
| 152 | + "id": "efc5aadf-87eb-4731-8697-eb0dd8d48b48", |
| 153 | + "status": "completed", |
| 154 | + "type": "transfer" |
| 155 | +} |
| 156 | +``` |
| 157 | +
|
| 158 | +> Note that the actual response will contain several fields in addition to those shown in this simplified example. |
| 159 | +
|
| 160 | +In protected cards, the only operations that can be performed without a signature are deposits _into_ the card. |
| 161 | +In order to transact _from_ a protected card to any destination, we'll need to sign the request. |
| 162 | +
|
| 163 | +Creating signed transactions from a protected card can be done in much the same way as the process for creating the protected cards themselves |
| 164 | +— that is, via normal transaction creation requests that include the `Digest` and `Signature` headers. |
| 165 | +
|
| 166 | +In this case, since the public key is not transmitted in the request body, |
| 167 | +the signature serves as a cryptographically strong assurance |
| 168 | +that the originator of the transaction is authorized to move funds from this card. |
| 169 | +More concretely, it proves that they have access to the private part of the key pair |
| 170 | +that's linked to the protected card in Uphold's internal records. |
0 commit comments