Skip to content

Commit 6b1d6d2

Browse files
committed
Document the Protected Cards feature
1 parent fcd4627 commit 6b1d6d2

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

_protected-cards.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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

Comments
 (0)