Skip to content

Commit

Permalink
feat: digital signature #132
Browse files Browse the repository at this point in the history
  • Loading branch information
dantio committed Dec 27, 2022
1 parent a8d5845 commit 9aedbb5
Show file tree
Hide file tree
Showing 20 changed files with 437 additions and 94 deletions.
13 changes: 13 additions & 0 deletions examples/restful/developer/keyManagement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// tslint:disable:no-console
import eBayApi from '../../../src/eBayApi.js';

const eBay = eBayApi.fromEnv();

(async () => {
try {
const signingKey = await eBay.developer.keyManagement.createSigningKey('ED25519');
console.log(JSON.stringify(signingKey, null, 2));
} catch (e) {
console.error(e);
}
})();
10 changes: 10 additions & 0 deletions examples/restful/sell/finances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// tslint:disable:no-console
import eBayApi from '../../../src/eBayApi.js';

const eBay = eBayApi.fromEnv();

eBay.sell.finances.sign.getSellerFundsSummary().then(result => {
console.log('result', JSON.stringify(result, null, 2));
}).catch(e => {
console.error(JSON.stringify(e, null, 2));
});
12 changes: 12 additions & 0 deletions examples/traditional/trading.GetAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// tslint:disable:no-console
import eBayApi from '../../src/eBayApi.js';

const eBay = eBayApi.fromEnv();
eBay.trading.GetAccount(null, {
sign: true
}).then(result => {
console.log(JSON.stringify(result, null, 2));
}).catch(e => {
console.error(e);
});

18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"@rollup/plugin-json": "^5.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.2.0",
"@rollup/plugin-virtual": "^3.0.1",
"@types/chai": "^4.3.4",
"@types/debug": "^4.1.7",
"@types/mocha": "^10.0.1",
Expand Down
11 changes: 10 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import bundleSize from 'rollup-plugin-bundle-size';
import virtual from '@rollup/plugin-virtual';

const pkg = require('./package.json');

Expand All @@ -27,6 +28,9 @@ export default [{
name: 'eBayApi',
exports: 'default',
sourcemap: false,
globals: {
crypto: 'crypto'
}
},
],
plugins
Expand All @@ -40,5 +44,10 @@ export default [{
},
],
context: 'window',
plugins
plugins: [
virtual({
crypto: `export function createHash() { return window.crypto.createHash(...arguments); }; export function sign() { return window.crypto.sign(...arguments); };`,
}),
...plugins
]
}]
138 changes: 138 additions & 0 deletions src/api/digitalSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {createHash, sign} from 'crypto';
import {Cipher, Headers} from '../types/index.js';

const beginPrivateKey = '-----BEGIN PRIVATE KEY-----';
const endPrivateKey = '-----END PRIVATE KEY-----';

// based on https://github.com/ebay/digital-signature-nodejs-sdk

/**
* Returns the current UNIX timestamp.
*
* @returns {number} The unix timestamp.
*/
const getUnixTimestamp = (): number => Date.now() / 1000 | 0;

const getSignatureParams = (payload: any) => [
...payload ? ['content-digest'] : [],
'x-ebay-signature-key',
'@method',
'@path',
'@authority'
];

const getSignatureParamsValue = (payload: any) => getSignatureParams(payload).map(param => `"${param}"`).join(' ');

/**
* Generates the 'Content-Digest' header value for the input payload.
*
* @param {any} payload The request payload.
* @param {string} cipher The algorithm used to calculate the digest.
* @returns {string} contentDigest The 'Content-Digest' header value.
*/
export const generateContentDigestValue = (payload: unknown, cipher: Cipher = 'sha256'): string => {
const payloadBuffer: Buffer = Buffer.from(typeof payload === 'string' ? payload : JSON.stringify(payload));

const hash = createHash(cipher).update(payloadBuffer).digest('base64');
const algo: string = cipher === 'sha512' ? 'sha-512' : 'sha-256';
return `${algo}=:${hash}:`;
};

export type SignatureComponents = {
method: string
authority: string // the host
path: string
}

/**
* Generates the base string.
*
* @param {any} headers The HTTP request headers.
* @param {SignatureComponents} signatureComponents The config.
* @param {any} payload The payload.
* @param {number} timestamp The timestamp.
* @returns {string} payload The base string.
*/
export function generateBaseString(headers: Headers, signatureComponents: SignatureComponents, payload: any, timestamp = getUnixTimestamp()): string {
try {
let baseString: string = '';
const signatureParams: string[] = getSignatureParams(payload);

signatureParams.forEach(param => {
baseString += `"${param.toLowerCase()}": `;

if (param.startsWith('@')) {
switch (param.toLowerCase()) {
case '@method':
baseString += signatureComponents.method;
break;
case '@authority':
baseString += signatureComponents.authority;
break;
case '@path':
baseString += signatureComponents.path;
break;
default:
throw new Error('Unknown pseudo header ' + param);
}
} else {
if (!headers[param]) {
throw new Error('Header ' + param + ' not included in message');
}
baseString += headers[param];
}

baseString += '\n';
});

baseString += `"@signature-params": (${getSignatureParamsValue(payload)});created=${timestamp}`;

return baseString;
} catch (ex: any) {
throw new Error(`Error calculating signature base: ${ex.message}`);
}
}

/**
* Generates the Signature-Input header value for the input payload.
*
* @param {any} payload The input config.
* @param {number} timestamp The timestamp.
* @returns {string} the 'Signature-Input' header value.
*/
export const generateSignatureInput = (payload: any, timestamp = getUnixTimestamp()): string => `sig1=(${getSignatureParamsValue(payload)});created=${timestamp}`;

/**
* Generates the 'Signature' header.
*
* @param {any} headers The HTTP headers.
* @param {string} privateKey The HTTP headers.
* @param {SignatureComponents} signatureComponents The signature components
* @param {any} payload The payload
* @param {number} timestamp The payload
* @returns {string} the signature header value.
*/
export function generateSignature(
headers: any,
privateKey: string,
signatureComponents: SignatureComponents,
payload: any,
timestamp = getUnixTimestamp()
): string {
const baseString = generateBaseString(headers, signatureComponents, payload, timestamp);

privateKey = privateKey.trim();
if (!privateKey.startsWith(beginPrivateKey)) {
privateKey = beginPrivateKey + '\n' + privateKey + '\n' + endPrivateKey;
}

const signatureBuffer = sign(
undefined, // If algorithm is undefined, then it is dependent upon the private key type.
Buffer.from(baseString),
privateKey
);

const signature = signatureBuffer.toString('base64');
return `sig1=:${signature}:`;
}

25 changes: 25 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import Auth from '../auth/index.js';
import {IEBayApiRequest} from '../request.js';
import {AppConfig} from '../types/index.js';
import Base from './base.js';
import {
generateContentDigestValue,
generateSignature,
generateSignatureInput,
SignatureComponents
} from './digitalSignature.js';

/**
* Superclass with Auth container.
Expand All @@ -18,4 +24,23 @@ export default abstract class Api extends Base {
this.auth = auth || new Auth(this.config, this.req);
}

getDigitalSignatureHeaders(signatureComponents: SignatureComponents, payload: any) {
if (!this.config.signature) {
return {};
}

const digitalSignatureHeaders = {
'x-ebay-enforce-signature': true, // enable digital signature validation
'x-ebay-signature-key': this.config.signature.jwe, // always contains JWE
...payload ? {
'content-digest': generateContentDigestValue(payload, this.config.signature.cipher ?? 'sha256')
} : {},
'signature-input': generateSignatureInput(payload)
};

return {
...digitalSignatureHeaders,
'signature': generateSignature(digitalSignatureHeaders, this.config.signature.privateKey, signatureComponents, payload)
};
}
}
9 changes: 5 additions & 4 deletions src/api/restful/developer/keyManagement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ export default class KeyManagement extends Restful {
}

/**
* his method creates keypairs.
* This method creates keypairs.
*/
public createSigningKey(data: { signingKeyCipher: string }) {
return this.post(`/signing_key`, data);
public createSigningKey(signingKeyCipher: 'ED25519' | 'RSA') {
return this.post(`/signing_key`, {
signingKeyCipher
});
}


/**
* This method returns the <b>Public Key</b>, <b>Public Key as JWE</b>,
* and metadata for a specified <code>signingKeyId</code> associated with the application key making the call.
Expand Down

0 comments on commit 9aedbb5

Please sign in to comment.