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