-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Document the Protected Cards feature
- Loading branch information
1 parent
da63eb1
commit 27da1a2
Showing
1 changed file
with
157 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
# 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. |