Skip to content

Commit

Permalink
Adding SignatureV4a implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Braiden Maggia committed Apr 29, 2024
1 parent 2414695 commit 9d4188c
Show file tree
Hide file tree
Showing 9 changed files with 554 additions and 172 deletions.
5 changes: 5 additions & 0 deletions .changeset/tall-pens-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/signature-v4": minor
---

Adding Signature V4a implementation
3 changes: 2 additions & 1 deletion packages/signature-v4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"@smithy/util-middleware": "workspace:^",
"@smithy/util-uri-escape": "workspace:^",
"@smithy/util-utf8": "workspace:^",
"tslib": "^2.6.2"
"tslib": "^2.6.2",
"elliptic": "^6.5.5"
},
"devDependencies": {
"@aws-crypto/sha256-js": "3.0.0",
Expand Down
197 changes: 29 additions & 168 deletions packages/signature-v4/src/SignatureV4.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import {
AwsCredentialIdentity,
ChecksumConstructor,
DateInput,
EventSigner,
EventSigningArguments,
FormattedEvent,
HashConstructor,
HeaderBag,
HttpRequest,
MessageSigner,
Provider,
RequestPresigner,
RequestPresigningArguments,
RequestSigner,
Expand All @@ -20,8 +15,6 @@ import {
StringSigner,
} from "@smithy/types";
import { toHex } from "@smithy/util-hex-encoding";
import { normalizeProvider } from "@smithy/util-middleware";
import { escapeUri } from "@smithy/util-uri-escape";
import { toUint8Array } from "@smithy/util-utf8";

import {
Expand All @@ -40,88 +33,34 @@ import {
TOKEN_HEADER,
TOKEN_QUERY_PARAM,
} from "./constants";
import { createScope, getSigningKey } from "./credentialDerivation";
import { getCanonicalHeaders } from "./getCanonicalHeaders";
import { getCanonicalQuery } from "./getCanonicalQuery";
import { getPayloadHash } from "./getPayloadHash";
import { HeaderFormatter } from "./HeaderFormatter";
import { hasHeader } from "./headerUtil";
import { moveHeadersToQuery } from "./moveHeadersToQuery";
import { prepareRequest } from "./prepareRequest";
import { iso8601 } from "./utilDate";
import {SignatureV4Base, SignatureV4CryptoInit, SignatureV4Init} from "./SignatureV4Base";
import {createSigV4Scope, getSigV4SigningKey} from "./credentialDerivation";

export interface SignatureV4Init {
/**
* The service signing name.
*/
service: string;

/**
* The region name or a function that returns a promise that will be
* resolved with the region name.
*/
region: string | Provider<string>;

/**
* The credentials with which the request should be signed or a function
* that returns a promise that will be resolved with credentials.
*/
credentials: AwsCredentialIdentity | Provider<AwsCredentialIdentity>;

/**
* A constructor function for a hash object that will calculate SHA-256 HMAC
* checksums.
*/
sha256?: ChecksumConstructor | HashConstructor;

/**
* Whether to uri-escape the request URI path as part of computing the
* canonical request string. This is required for every AWS service, except
* Amazon S3, as of late 2017.
*
* @default [true]
*/
uriEscapePath?: boolean;

/**
* Whether to calculate a checksum of the request body and include it as
* either a request header (when signing) or as a query string parameter
* (when presigning). This is required for AWS Glacier and Amazon S3 and optional for
* every other AWS service as of late 2017.
*
* @default [true]
*/
applyChecksum?: boolean;
}

export interface SignatureV4CryptoInit {
sha256: ChecksumConstructor | HashConstructor;
}

export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigner, EventSigner, MessageSigner {
private readonly service: string;
private readonly regionProvider: Provider<string>;
private readonly credentialProvider: Provider<AwsCredentialIdentity>;
private readonly sha256: ChecksumConstructor | HashConstructor;
private readonly uriEscapePath: boolean;
private readonly applyChecksum: boolean;
export class SignatureV4 extends SignatureV4Base implements RequestPresigner, RequestSigner, StringSigner, EventSigner, MessageSigner {
private readonly headerFormatter = new HeaderFormatter();

constructor({
applyChecksum,
credentials,
region,
service,
sha256,
uriEscapePath = true,
}: SignatureV4Init & SignatureV4CryptoInit) {
this.service = service;
this.sha256 = sha256;
this.uriEscapePath = uriEscapePath;
// default to true if applyChecksum isn't set
this.applyChecksum = typeof applyChecksum === "boolean" ? applyChecksum : true;
this.regionProvider = normalizeProvider(region);
this.credentialProvider = normalizeProvider(credentials);
applyChecksum,
credentials,
region,
service,
sha256,
uriEscapePath = true,
}: SignatureV4Init & SignatureV4CryptoInit) {
super({
applyChecksum: applyChecksum,
credentials: credentials,
region: region,
service: service,
sha256: sha256,
uriEscapePath: uriEscapePath
});
}

public async presign(originalRequest: HttpRequest, options: RequestPresigningArguments = {}): Promise<HttpRequest> {
Expand All @@ -138,14 +77,14 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
this.validateResolvedCredentials(credentials);
const region = signingRegion ?? (await this.regionProvider());

const { longDate, shortDate } = formatDate(signingDate);
const { longDate, shortDate } = this.formatDate(signingDate);
if (expiresIn > MAX_PRESIGNED_TTL) {
return Promise.reject(
"Signature version 4 presigned URLs" + " must have an expiration date less than one week in" + " the future"
);
}

const scope = createScope(shortDate, region, signingService ?? this.service);
const scope = createSigV4Scope(shortDate, region, signingService ?? this.service);
const request = moveHeadersToQuery(prepareRequest(originalRequest), { unhoistableHeaders });

if (credentials.sessionToken) {
Expand All @@ -157,7 +96,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
request.query[EXPIRES_QUERY_PARAM] = expiresIn.toString(10);

const canonicalHeaders = getCanonicalHeaders(request, unsignableHeaders, signableHeaders);
request.query[SIGNED_HEADERS_QUERY_PARAM] = getCanonicalHeaderList(canonicalHeaders);
request.query[SIGNED_HEADERS_QUERY_PARAM] = this.getCanonicalHeaderList(canonicalHeaders);

request.query[SIGNATURE_QUERY_PARAM] = await this.getSignature(
longDate,
Expand Down Expand Up @@ -190,8 +129,8 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
{ signingDate = new Date(), priorSignature, signingRegion, signingService }: EventSigningArguments
): Promise<string> {
const region = signingRegion ?? (await this.regionProvider());
const { shortDate, longDate } = formatDate(signingDate);
const scope = createScope(shortDate, region, signingService ?? this.service);
const { shortDate, longDate } = this.formatDate(signingDate);
const scope = createSigV4Scope(shortDate, region, signingService ?? this.service);
const hashedPayload = await getPayloadHash({ headers: {}, body: payload } as any, this.sha256);
const hash = new this.sha256();
hash.update(headers);
Expand Down Expand Up @@ -236,7 +175,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
const credentials = await this.credentialProvider();
this.validateResolvedCredentials(credentials);
const region = signingRegion ?? (await this.regionProvider());
const { shortDate } = formatDate(signingDate);
const { shortDate } = this.formatDate(signingDate);

const hash = new this.sha256(await this.getSigningKey(credentials, region, shortDate, signingService));
hash.update(toUint8Array(stringToSign));
Expand All @@ -257,8 +196,8 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
this.validateResolvedCredentials(credentials);
const region = signingRegion ?? (await this.regionProvider());
const request = prepareRequest(requestToSign);
const { longDate, shortDate } = formatDate(signingDate);
const scope = createScope(shortDate, region, signingService ?? this.service);
const { longDate, shortDate } = this.formatDate(signingDate);
const scope = createSigV4Scope(shortDate, region, signingService ?? this.service);

request.headers[AMZ_DATE_HEADER] = longDate;
if (credentials.sessionToken) {
Expand All @@ -281,75 +220,19 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
request.headers[AUTH_HEADER] =
`${ALGORITHM_IDENTIFIER} ` +
`Credential=${credentials.accessKeyId}/${scope}, ` +
`SignedHeaders=${getCanonicalHeaderList(canonicalHeaders)}, ` +
`SignedHeaders=${this.getCanonicalHeaderList(canonicalHeaders)}, ` +
`Signature=${signature}`;

return request;
}

private createCanonicalRequest(request: HttpRequest, canonicalHeaders: HeaderBag, payloadHash: string): string {
const sortedHeaders = Object.keys(canonicalHeaders).sort();
return `${request.method}
${this.getCanonicalPath(request)}
${getCanonicalQuery(request)}
${sortedHeaders.map((name) => `${name}:${canonicalHeaders[name]}`).join("\n")}
${sortedHeaders.join(";")}
${payloadHash}`;
}

private async createStringToSign(
longDate: string,
credentialScope: string,
canonicalRequest: string
): Promise<string> {
const hash = new this.sha256();
hash.update(toUint8Array(canonicalRequest));
const hashedRequest = await hash.digest();

return `${ALGORITHM_IDENTIFIER}
${longDate}
${credentialScope}
${toHex(hashedRequest)}`;
}

private getCanonicalPath({ path }: HttpRequest): string {
if (this.uriEscapePath) {
// Non-S3 services, we normalize the path and then double URI encode it.
// Ref: "Remove Dot Segments" https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
const normalizedPathSegments = [];
for (const pathSegment of path.split("/")) {
if (pathSegment?.length === 0) continue;
if (pathSegment === ".") continue;
if (pathSegment === "..") {
normalizedPathSegments.pop();
} else {
normalizedPathSegments.push(pathSegment);
}
}
// Joining by single slashes to remove consecutive slashes.
const normalizedPath = `${path?.startsWith("/") ? "/" : ""}${normalizedPathSegments.join("/")}${
normalizedPathSegments.length > 0 && path?.endsWith("/") ? "/" : ""
}`;

// Double encode and replace non-standard characters !'()* according to RFC 3986
const doubleEncoded = escapeUri(normalizedPath);
return doubleEncoded.replace(/%2F/g, "/");
}

// For S3, we shouldn't normalize the path. For example, object name
// my-object//example//photo.user should not be normalized to
// my-object/example/photo.user
return path;
}

private async getSignature(
longDate: string,
credentialScope: string,
keyPromise: Promise<Uint8Array>,
canonicalRequest: string
): Promise<string> {
const stringToSign = await this.createStringToSign(longDate, credentialScope, canonicalRequest);
const stringToSign = await this.createStringToSign(longDate, credentialScope, canonicalRequest, ALGORITHM_IDENTIFIER);

const hash = new this.sha256(await keyPromise);
hash.update(toUint8Array(stringToSign));
Expand All @@ -362,28 +245,6 @@ ${toHex(hashedRequest)}`;
shortDate: string,
service?: string
): Promise<Uint8Array> {
return getSigningKey(this.sha256, credentials, shortDate, region, service || this.service);
}

private validateResolvedCredentials(credentials: unknown) {
if (
typeof credentials !== "object" ||
// @ts-expect-error: Property 'accessKeyId' does not exist on type 'object'.ts(2339)
typeof credentials.accessKeyId !== "string" ||
// @ts-expect-error: Property 'secretAccessKey' does not exist on type 'object'.ts(2339)
typeof credentials.secretAccessKey !== "string"
) {
throw new Error("Resolved credential object is not valid");
}
return getSigV4SigningKey(this.sha256, credentials, shortDate, region, service || this.service);
}
}

const formatDate = (now: DateInput): { longDate: string; shortDate: string } => {
const longDate = iso8601(now).replace(/[\-:]/g, "");
return {
longDate,
shortDate: longDate.slice(0, 8),
};
};

const getCanonicalHeaderList = (headers: object): string => Object.keys(headers).sort().join(";");
Loading

0 comments on commit 9d4188c

Please sign in to comment.