Last active
January 21, 2025 23:39
-
-
Save perpil/9eef495eddc686db377f77b7836efc2a to your computer and use it in GitHub Desktop.
Viewer request code for sigv4 signing in a cloudfront function to furl. This code calculates the signature, but it doesn work because the Authorization header is stripped by cloudfront. Use CloudFront Lambda OAC instead.
This file contains hidden or 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
| const crypto = require('crypto'); | |
| const querystring = require('querystring'); | |
| function hmac(key, string, encoding) { | |
| return crypto | |
| .createHmac('sha256', key) | |
| .update(string, 'utf8') | |
| .digest(encoding); | |
| } | |
| function hash(string, encoding) { | |
| return crypto.createHash('sha256').update(string, 'utf8').digest(encoding); | |
| } | |
| // This function assumes the string has already been percent encoded | |
| function encodeRfc3986(urlEncodedString) { | |
| return urlEncodedString.replace(/[!'()*]/g, function (c) { | |
| return '%' + c.charCodeAt(0).toString(16).toUpperCase(); | |
| }); | |
| } | |
| function encodeRfc3986Full(str) { | |
| return encodeRfc3986(encodeURIComponent(str)); | |
| } | |
| var HEADERS_TO_IGNORE = [ | |
| /*'authorization', | |
| 'connection', | |
| 'x-amzn-trace-id', | |
| 'user-agent', | |
| 'expect', | |
| 'presigned-expires', | |
| 'range',*/ | |
| ]; | |
| // request: { path | body, [host], [method], [headers] } | |
| // credentials: { accessKeyId, secretAccessKey, [sessionToken] } | |
| let tRequest,tCredentials,tService,tRegion,tExtraHeadersToIgnore,tExtraHeadersToInclude,tParsedPath,tDatetime; | |
| function RequestSigner(request, credentials) { | |
| var headers = (request.headers = request.headers || {}); | |
| tRequest = request; | |
| tCredentials = credentials; | |
| tService = 'lambda'; | |
| tRegion = request.host.match(/^[^\.]+\.lambda-url\.(.*?)\.on\.aws$/)[1]; | |
| if (!request.method && request.body) request.method = 'POST'; | |
| if (!headers.Host && !headers.host) { | |
| headers.Host = request.host; | |
| } | |
| tExtraHeadersToIgnore = | |
| request.extraHeadersToIgnore || {}; | |
| tExtraHeadersToInclude = | |
| request.extraHeadersToInclude || {}; | |
| } | |
| function prepareRequest() { | |
| parsePath(); | |
| var request = tRequest, | |
| headers = request.headers, | |
| query; | |
| if (!request.doNotModifyHeaders) { | |
| if (request.body && !headers['Content-Type'] && !headers['content-type']) | |
| headers['Content-Type'] = | |
| 'application/x-www-form-urlencoded; charset=utf-8'; | |
| if ( | |
| request.body && | |
| !headers['Content-Length'] && | |
| !headers['content-length'] | |
| ) | |
| headers['Content-Length'] = Buffer.byteLength(request.body); | |
| if ( | |
| tCredentials.sessionToken && | |
| !headers['X-Amz-Security-Token'] && | |
| !headers['x-amz-security-token'] | |
| ) | |
| headers['X-Amz-Security-Token'] = tCredentials.sessionToken; | |
| headers['X-Amz-Date'] = getDateTime(); | |
| } | |
| //delete headers.Authorization; | |
| //delete headers.authorization; | |
| } | |
| function sign() { | |
| if (tParsedPath) prepareRequest(); | |
| tRequest.headers.Authorization = authHeader(); | |
| tRequest.path = formatPath(); | |
| return tRequest; | |
| } | |
| function getDateTime() { | |
| if (!tDatetime) { | |
| var headers = tRequest.headers, | |
| date = new Date(headers.Date || headers.date || new Date()); | |
| tDatetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, ''); | |
| } | |
| return tDatetime; | |
| } | |
| function getDate() { | |
| return getDateTime().substr(0, 8); | |
| } | |
| function authHeader() { | |
| return `AWS4-HMAC-SHA256 Credential=${ | |
| tCredentials.accessKeyId | |
| }/${credentialString()}, SignedHeaders=${signedHeaders()}, Signature=${signature()}`; | |
| } | |
| function signature() { | |
| var date = getDate(); | |
| var kCredentials = hmac( | |
| hmac( | |
| hmac(hmac('AWS4' + tCredentials.secretAccessKey, date), tRegion), | |
| tService | |
| ), | |
| 'aws4_request' | |
| ); | |
| return hmac(kCredentials, stringToSign(), 'hex'); | |
| } | |
| function stringToSign() { | |
| return [ | |
| 'AWS4-HMAC-SHA256', | |
| getDateTime(), | |
| credentialString(), | |
| hash(canonicalString(), 'hex'), | |
| ].join('\n'); | |
| } | |
| function canonicalString() { | |
| if (!tParsedPath) prepareRequest(); | |
| var pathStr = tParsedPath.path, | |
| query = tParsedPath.query, | |
| headers = tRequest.headers, | |
| queryStr = '', | |
| decodePath = tRequest.doNotEncodePath, | |
| bodyHash; | |
| bodyHash = | |
| headers['X-Amz-Content-Sha256'] || | |
| headers['x-amz-content-sha256'] || | |
| hash(tRequest.body || '', 'hex'); | |
| if (query) { | |
| var reducedQuery = Object.keys(query).reduce(function (obj, key) { | |
| if (!key) return obj; | |
| obj[encodeRfc3986Full(key)] = !Array.isArray(query[key]) | |
| ? query[key] | |
| : query[key]; | |
| return obj; | |
| }, {}); | |
| var encodedQueryPieces = []; | |
| Object.keys(reducedQuery) | |
| .sort() | |
| .forEach(function (key) { | |
| if (!Array.isArray(reducedQuery[key])) { | |
| encodedQueryPieces.push( | |
| key + '=' + encodeRfc3986Full(reducedQuery[key]) | |
| ); | |
| } else { | |
| reducedQuery[key] | |
| .map(encodeRfc3986Full) | |
| .sort() | |
| .forEach(function (val) { | |
| encodedQueryPieces.push(key + '=' + val); | |
| }); | |
| } | |
| }); | |
| queryStr = encodedQueryPieces.join('&'); | |
| } | |
| if (pathStr !== '/') { | |
| pathStr = pathStr | |
| .split('/') | |
| .reduce(function (path, piece) { | |
| if (decodePath) piece = decodeURIComponent(piece.replace(/\+/g, ' ')); | |
| path.push(encodeRfc3986Full(piece)); | |
| return path; | |
| }, []) | |
| .join('/'); | |
| if (pathStr[0] !== '/') pathStr = '/' + pathStr; | |
| } | |
| return [ | |
| tRequest.method || 'GET', | |
| pathStr, | |
| queryStr, | |
| canonicalHeaders() + '\n', | |
| signedHeaders(), | |
| bodyHash, | |
| ].join('\n'); | |
| } | |
| function canonicalHeaders() { | |
| var headers = tRequest.headers; | |
| function trimAll(header) { | |
| return header.toString().trim().replace(/\s+/g, ' '); | |
| } | |
| return Object.keys(headers) | |
| .filter(function (key) { | |
| return !HEADERS_TO_IGNORE.includes(key.toLowerCase()); | |
| }) | |
| .sort(function (a, b) { | |
| return a.toLowerCase() < b.toLowerCase() ? -1 : 1; | |
| }) | |
| .map(function (key) { | |
| return key.toLowerCase() + ':' + trimAll(headers[key]); | |
| }) | |
| .join('\n'); | |
| } | |
| function signedHeaders() { | |
| var extraHeadersToInclude = tExtraHeadersToInclude, | |
| extraHeadersToIgnore = tExtraHeadersToIgnore; | |
| return Object.keys(tRequest.headers) | |
| .map(function (key) { | |
| return key.toLowerCase(); | |
| }) | |
| .filter(function (key) { | |
| return ( | |
| extraHeadersToInclude[key] || | |
| (!HEADERS_TO_IGNORE.includes(key) && !extraHeadersToIgnore[key]) | |
| ); | |
| }) | |
| .sort() | |
| .join(';'); | |
| } | |
| function credentialString() { | |
| return [getDate(), tRegion, tService, 'aws4_request'].join('/'); | |
| } | |
| function parsePath() { | |
| var path = tRequest.path || '/'; | |
| if (/[^0-9A-Za-z;,/?:@&=+$\-_.!~*'()#%]/.test(path)) { | |
| path = encodeURI(decodeURI(path)); | |
| } | |
| var queryIx = path.indexOf('?'), | |
| query = null; | |
| if (queryIx >= 0) { | |
| query = querystring.parse(path.slice(queryIx + 1)); | |
| path = path.slice(0, queryIx); | |
| } | |
| tParsedPath = { | |
| path: path, | |
| query: query, | |
| }; | |
| } | |
| function formatPath() { | |
| var path = tParsedPath.path, | |
| query = tParsedPath.query; | |
| if (!query) return path; | |
| // Services don't support empty query string keys | |
| if (query[''] != null) delete query['']; | |
| return path + '?' + encodeRfc3986(querystring.stringify(query)); | |
| } | |
| function handler(event) { | |
| const request = event.request; | |
| const qs = request.querystring; | |
| let qo = {}; | |
| Object.keys(qs).forEach(k => qo[k] = qs[k].value); | |
| let q = querystring.stringify(qo) | |
| let requestOptions = { | |
| host: request.headers.host.value, | |
| path: request.uri + q?`?${q}`:'' | |
| }; | |
| RequestSigner(requestOptions,{secretAccessKey: 'FIXME', accessKeyId: 'FIXME'}); | |
| sign(); | |
| request.headers['authorization'] = {value:requestOptions.headers.Authorization}; | |
| request.headers['x-amz-date'] = {value:requestOptions.headers['X-Amz-Date']}; | |
| return request; | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've updated the description to note that it doesn't work so future readers don't get fooled.
Using an edge lambda is a valid way of calculating the
x-amz-content-sha256so you don't need to do it on the client. The use case where it makes sense is that you need a custom url for your furl or you need to add WAF. If you don't need either of those, you'll get lower latency by turning off IAM on your furl and using it directly. Without doing additional authentication in your edge lambda, the threat profile is similar to having a wide open lambda.