Skip to content
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

Document the Protected Cards feature #216

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions _protected-cards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Protected Cards

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).

The instructions below describe in detail how to make use of this feature.

## Generate a Key Pair

> Create an EdDSA cryptographic key pair, e.g. using the [tweetnacl](https://www.npmjs.com/package/tweetnacl) package:

```js
const nacl = require('tweetnacl');
const keyPair = nacl.sign.keyPair(); // Generate an Ed25519 key pair
const publicKey = Buffer.from(keyPair.publicKey).toString('hex');
const secretKey = Buffer.from(keyPair.secretKey).toString('hex');
```

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`).

This step should be performed on your server-side, and the private key must be stored securely.
Never expose or transmit the private key to Uphold, or any other third party outside your control.

## Create a Protected Card

> Construct the body of the request to create a card:

```js
const data = {
currency: 'USD',
label: 'My Protected Card',
publicKey: publicKey
}
```

> Create a SHA-256 (or SHA-512) hash of the request body, and store it as a base64-encoded digest:

```js
const crypto = require('crypto');
const digest = crypto.createHash('sha256').update(JSON.stringify(data)).digest('base64');
```

> Create the signature, e.g. using the [http-request-signature](https://www.npmjs.com/package/http-request-signature) package:

```js
const { sign } = require('http-request-signature');
const signature = sign({
headers: {
digest: `SHA-256=${digest}` // This must match the `Digest` header
}, // that will be sent in the request.
keyId: 'primary', // We require the `keyId` to be "primary".
secretKey
}, { algorithm: 'ed25519' }); // We only support the `ed25519` algorithm.
```

> Submit the request including the `Digest` and the `Signature` headers, as well as the data used to generate them:

```bash
$ curl 'https://api.uphold.com/v0/me/cards' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <token>' \
-H 'Digest: SHA-256=<digest>' \
-H 'Signature: <signature>' \
-H 'Content-Type: application/json' \
-d '{ "currency": "USD", "label": "My Protected Card", "publicKey": "<publicKey>" }'
```

> The response should be a [card object](https://uphold.com/en/developer/api/documentation/#card-object)
> (sample output truncated for conciseness):

```json
{
"available": "0.00",
"balance": "0.00",
"currency": "USD",
"id": "71064207-b557-4808-ac33-e4eb86d78a01",
"label": "My Protected Card"
}
```

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).

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.

<aside class="notice">
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.
In the example shown here, it must equal the output of <code>JSON.stringify()</code>, which is fed into <code>crypto.createHash()</code>.
</aside>

## Create Signed Transactions

> Construct the body of the request to create a transaction from a protected card:

```js
const data = {
denomination: {
amount: '10',
currency: USD'
},
destination: '<address>'
}
```

> Generate the digest and signature of the request data:

```js
const crypto = require('crypto');
const digest = crypto.createHash('256').update(JSON.stringify(data)).digest('base64');

const { sign } = require('http-request-signature');
const signature = sign({
headers: {
digest: `SHA-256=${digest}`
},
keyId: 'primary',
secretKey
}, { algorithm: 'ed25519' });
```

> Submit a request for creating a signed transaction, using the id of the protected card in the URL parameters:

```bash
$ curl 'https://api.uphold.com/v0/me/cards/<id>/transactions?commit=true' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <token>' \
-H 'Digest: SHA-256=<digest>' \
-H 'Signature: <signature>' \
-H 'Content-Type: application/json' \
-d <data>
```

> Uphold's server verifies that the transaction's signature is correct and proceeds with committing the transaction
> (sample output truncated for conciseness):

```json
{
"createdAt": "2017-06-26T17:17:57.532Z",
"denomination": {
"pair": "USDUSD",
"rate": "1.00",
"amount": "1.00",
"currency": "USD"
},
"id": "efc5aadf-87eb-4731-8697-eb0dd8d48b48",
"status": "completed",
"type": "transfer"
}
```

> Note that the actual response will contain several fields in addition to those shown in this simplified example.

In protected cards, the only operations that can be performed without a signature are deposits _into_ the card.
In order to transact _from_ a protected card to any destination, we'll need to sign the request.

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.

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.
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.