sigv4.v

4.8 KB · raw

1//! AWS Signature Version 4, the part S3 needs.2//!3//! Signing is a fixed recipe: canonicalise the request, hash it, derive a key4//! from the secret for that day and region, and sign the string-to-sign. It is5//! all here in the open, and pinned to the published AWS `get-vanilla` vector,6//! so the signer is checked rather than trusted.78module remote910import crypto.hmac11import crypto.sha25612import time1314pub struct Signer {15pub:16	access_key string17	secret_key string18	region     string19	service    string20	// S3 signs the payload hash as well; the AWS test vectors for the generic21	// service do not. The field exists so the rest of the algorithm can be22	// checked against those vectors.23	sign_payload bool = true24}2526// sign returns the headers that authenticate one request: every header passed27// in, plus host, x-amz-date and (for S3) x-amz-content-sha256, plus the28// Authorization header itself. `now` is used exactly as given, so the caller29// decides the zone — `time.now()` is local, and S3 wants UTC.30pub fn (signer Signer) sign(method string, url string, query string, headers map[string]string, body []u8, now time.Time) map[string]string {31	parts := split_url(url)32	payload_hash := sha256_hex(body)33	amz_date := amz_timestamp(now)3435	mut signed := map[string]string{}36	for name, value in headers {37		signed[name.to_lower().trim_space()] = value.trim_space()38	}39	signed['host'] = parts.host40	signed['x-amz-date'] = amz_date41	if signer.sign_payload {42		signed['x-amz-content-sha256'] = payload_hash43	}4445	mut names := signed.keys()46	names.sort()47	mut canonical_headers := ''48	for name in names {49		canonical_headers += '${name}:${signed[name]}\n'50	}51	signed_names := names.join(';')52	canonical_request := '${method}\n${parts.path}\n${canonical_query(query)}\n${canonical_headers}\n${signed_names}\n${payload_hash}'5354	date := amz_date[..8]55	scope := '${date}/${signer.region}/${signer.service}/aws4_request'56	string_to_sign := 'AWS4-HMAC-SHA256\n${amz_date}\n${scope}\n${sha256_hex(canonical_request.bytes())}'57	signature :=58		hmac.new(signing_key(signer, date), string_to_sign.bytes(), sha256.sum256, 64).hex()5960	mut out := signed.clone()61	out['authorization'] = 'AWS4-HMAC-SHA256 Credential=${signer.access_key}/${scope}, SignedHeaders=${signed_names}, Signature=${signature}'62	return out63}6465// signing_key is the HMAC chain: the secret becomes a key for the day, then for66// the region, then for the service, then for the request.67fn signing_key(signer Signer, date string) []u8 {68	k_date := hmac.new('AWS4${signer.secret_key}'.bytes(), date.bytes(), sha256.sum256, 64)69	k_region := hmac.new(k_date, signer.region.bytes(), sha256.sum256, 64)70	k_service := hmac.new(k_region, signer.service.bytes(), sha256.sum256, 64)71	return hmac.new(k_service, 'aws4_request'.bytes(), sha256.sum256, 64)72}7374// sha256_hex is the digest form SigV4 puts in the canonical request.75fn sha256_hex(data []u8) string {76	return sha256.sum256(data).hex()77}7879// amz_timestamp is the one date format SigV4 uses.80pub fn amz_timestamp(moment time.Time) string {81	return '${moment.year}${pad2(moment.month)}${pad2(moment.day)}T${pad2(moment.hour)}${pad2(moment.minute)}${pad2(moment.second)}Z'82}8384fn pad2(value int) string {85	return if value < 10 { '0${value}' } else { '${value}' }86}8788pub struct UrlParts {89pub:90	host string // host[:port], as it appears in the Host header91	path string // canonical URI, already encoded92}9394// split_url takes a URL apart for the canonical request. The URL is the one95// that will be sent, already encoded by its builder, so its path is signed as96// it stands: encoding it again would sign `%2520` for a `%20` on the wire.97pub fn split_url(url string) UrlParts {98	mut rest := url99	if at := rest.index('://') {100		rest = rest[at + 3..]101	}102	mut host := rest103	mut path := '/'104	if slash := rest.index('/') {105		host = rest[..slash]106		path = rest[slash..]107	}108	if question := path.index('?') {109		path = path[..question]110	}111	return UrlParts{112		host: host113		path: path114	}115}116117// canonical_query sorts the query parameters and encodes them the way SigV4118// wants them.119pub fn canonical_query(query string) string {120	if query == '' {121		return ''122	}123	mut pairs := []string{}124	for part in query.split('&') {125		if part == '' {126			continue127		}128		if at := part.index('=') {129			pairs << '${encode(part[..at], false)}=${encode(part[at + 1..], false)}'130		} else {131			pairs << '${encode(part, false)}='132		}133	}134	pairs.sort()135	return pairs.join('&')136}137138// encode is RFC 3986: unreserved characters stay, everything else becomes %XX.139// `keep_slash` is for paths, where a slash separates key segments.140pub fn encode(text string, keep_slash bool) string {141	mut out := ''142	for byte in text.bytes() {143		if (byte >= `A` && byte <= `Z`) || (byte >= `a` && byte <= `z`)144			|| (byte >= `0` && byte <= `9`) || byte in [`-`, `_`, `.`, `~`]145			|| (keep_slash && byte == `/`) {146			out += byte.ascii_str()147		} else {148			out += '%${byte.hex().to_upper()}'149		}150	}151	return out152}