diff --git a/bin/aws-s3 b/bin/aws-s3
new file mode 120000
--- /dev/null
+++ b/bin/aws-s3
@@ -0,0 +1 @@
+../scripts/utils/aws-s3.php
\ No newline at end of file
diff --git a/scripts/utils/aws-s3.php b/scripts/utils/aws-s3.php
new file mode 100755
--- /dev/null
+++ b/scripts/utils/aws-s3.php
@@ -0,0 +1,22 @@
+#!/usr/bin/env php
+<?php
+
+$root = dirname(dirname(dirname(__FILE__)));
+require_once $root.'/scripts/__init_script__.php';
+
+$args = new PhutilArgumentParser($argv);
+$args->setTagline(pht('AWS CLI Client for S3'));
+$args->setSynopsis(<<<EOSYNOPSIS
+**aws-s3** __command__ [__options__]
+    Upload and download data from Amazon Simple Storage Service (S3).
+
+EOSYNOPSIS
+  );
+$args->parseStandardArguments();
+
+$workflows = id(new PhutilClassMapQuery())
+  ->setAncestorClass('PhutilAWSS3ManagementWorkflow')
+  ->execute();
+
+$workflows[] = new PhutilHelpArgumentWorkflow();
+$args->parseWorkflows($workflows);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -85,7 +85,10 @@
     'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
     'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
     'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
+    'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php',
     'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
+    'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php',
+    'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php',
     'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php',
     'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php',
     'PhutilAggregateException' => 'error/PhutilAggregateException.php',
@@ -603,7 +606,10 @@
     'PhutilAWSEC2Future' => 'PhutilAWSFuture',
     'PhutilAWSException' => 'Exception',
     'PhutilAWSFuture' => 'FutureProxy',
+    'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow',
     'PhutilAWSS3Future' => 'PhutilAWSFuture',
+    'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
+    'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow',
     'PhutilAWSv4Signature' => 'Phobject',
     'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
     'PhutilAggregateException' => 'Exception',
diff --git a/src/future/aws/PhutilAWSFuture.php b/src/future/aws/PhutilAWSFuture.php
--- a/src/future/aws/PhutilAWSFuture.php
+++ b/src/future/aws/PhutilAWSFuture.php
@@ -3,11 +3,13 @@
 abstract class PhutilAWSFuture extends FutureProxy {
 
   private $future;
-  private $awsAccessKey;
-  private $awsPrivateKey;
-  private $awsRegion;
-  private $builtRequest;
-  private $params;
+  private $accessKey;
+  private $secretKey;
+  private $region;
+  private $httpMethod = 'GET';
+  private $path = '/';
+  private $params = array();
+  private $endpoint;
 
   abstract public function getServiceName();
 
@@ -15,73 +17,101 @@
     parent::__construct(null);
   }
 
-  public function setAWSKeys($access, $private) {
-    $this->awsAccessKey = $access;
-    $this->awsPrivateKey = $private;
+  public function setAccessKey($access_key) {
+    $this->accessKey = $access_key;
     return $this;
   }
 
-  public function getAWSAccessKey() {
-    return $this->awsAccessKey;
+  public function getAccessKey() {
+    return $this->accessKey;
   }
 
-  public function getAWSPrivateKey() {
-    return $this->awsPrivateKey;
+  public function setSecretKey(PhutilOpaqueEnvelope $secret_key) {
+    $this->secretKey = $secret_key;
+    return $this;
+  }
+
+  public function getSecretKey() {
+    return $this->secretKey;
   }
 
-  public function getAWSRegion() {
-    return $this->awsRegion;
+  public function getRegion() {
+    return $this->region;
   }
 
-  public function setAWSRegion($region) {
-    $this->awsRegion = $region;
+  public function setRegion($region) {
+    $this->region = $region;
     return $this;
   }
 
-  public function getHost() {
-    $host = $this->getServiceName().'.'.$this->awsRegion.'.amazonaws.com';
-    return $host;
+  public function setEndpoint($endpoint) {
+    $this->endpoint = $endpoint;
+    return $this;
   }
 
-  public function setRawAWSQuery($action, array $params = array()) {
-    $this->params = $params;
-    $this->params['Action'] = $action;
+  public function getEndpoint() {
+    return $this->endpoint;
+  }
+
+  public function setHTTPMethod($method) {
+    $this->httpMethod = $method;
     return $this;
   }
 
-  protected function getProxiedFuture() {
-    if (!$this->future) {
-      $params = $this->params;
+  public function getHTTPMethod() {
+    return $this->httpMethod;
+  }
 
-      if (!$this->params) {
-        throw new Exception(
-          pht(
-            'You must %s!',
-            'setRawAWSQuery()'));
-      }
+  public function setPath($path) {
+    $this->path = $path;
+    return $this;
+  }
 
-      if (!$this->getAWSAccessKey()) {
-        throw new Exception(
-          pht(
-            'You must %s!',
-            'setAWSKeys()'));
-      }
+  public function getPath() {
+    return $this->path;
+  }
 
-      $params['AWSAccessKeyId'] = $this->getAWSAccessKey();
-      $params['Version']        = '2013-10-15';
-      $params['Timestamp']      = date('c');
+  protected function getParameters() {
+    $params = $this->params;
+    return $params;
+  }
 
-      $params = $this->sign($params);
+  protected function getProxiedFuture() {
+    if (!$this->future) {
+      $params = $this->getParameters();
+      $method = $this->getHTTPMethod();
+      $host = $this->getEndpoint();
+      $path = $this->getPath();
 
-      $uri = new PhutilURI('http://'.$this->getHost().'/');
-      $uri->setQueryParams($params);
+      $uri = id(new PhutilURI("https://{$host}/"))
+        ->setPath($path)
+        ->setQueryParams($params);
 
-      $this->future = new HTTPFuture($uri);
+      $future = id(new HTTPSFuture($uri))
+        ->setMethod($method);
+
+      $this->signRequest($future);
+
+      $this->future = $future;
     }
 
     return $this->future;
   }
 
+  protected function signRequest(HTTPSFuture $future) {
+    $access_key = $this->getAccessKey();
+    $secret_key = $this->getSecretKey();
+
+    $region = $this->getRegion();
+
+    id(new PhutilAWSv4Signature())
+      ->setRegion($region)
+      ->setService($this->getServiceName())
+      ->setAccessKey($access_key)
+      ->setSecretKey($secret_key)
+      ->signRequest($future);
+  }
+
   protected function didReceiveResult($result) {
     list($status, $body, $headers) = $result;
 
@@ -101,7 +131,8 @@
       );
       if ($xml) {
         $params['RequestID'] = $xml->RequestID[0];
-        foreach ($xml->Errors[0] as $error) {
+        $errors = array($xml->Error);
+        foreach ($errors as $error) {
           $params['Errors'][] = array($error->Code, $error->Message);
         }
       }
@@ -112,36 +143,4 @@
     return $xml;
   }
 
-  /**
-   * http://bit.ly/wU0JFh
-   */
-  private function sign(array $params) {
-
-    $params['SignatureMethod'] = 'HmacSHA256';
-    $params['SignatureVersion'] = '2';
-
-    ksort($params);
-
-    $pstr = array();
-    foreach ($params as $key => $value) {
-      $pstr[] = rawurlencode($key).'='.rawurlencode($value);
-    }
-    $pstr = implode('&', $pstr);
-
-    $sign = "GET"."\n".
-            strtolower($this->getHost())."\n".
-            "/"."\n".
-            $pstr;
-
-    $hash = hash_hmac(
-      'sha256',
-      $sign,
-      $this->getAWSPrivateKey(),
-      $raw_ouput = true);
-
-    $params['Signature'] = base64_encode($hash);
-
-    return $params;
-  }
-
 }
diff --git a/src/future/aws/PhutilAWSS3Future.php b/src/future/aws/PhutilAWSS3Future.php
--- a/src/future/aws/PhutilAWSS3Future.php
+++ b/src/future/aws/PhutilAWSS3Future.php
@@ -2,8 +2,42 @@
 
 final class PhutilAWSS3Future extends PhutilAWSFuture {
 
+  private $bucket;
+
   public function getServiceName() {
     return 's3';
   }
 
+  public function setBucket($bucket) {
+    $this->bucket = $bucket;
+    return $this;
+  }
+
+  public function getBucket() {
+    return $this->bucket;
+  }
+
+  public function setParametersForGetObject($key) {
+    $bucket = $this->getBucket();
+
+    $this->setHTTPMethod('GET');
+    $this->setPath($bucket.'/'.$key);
+
+    return $this;
+  }
+
+  protected function didReceiveResult($result) {
+    list($status, $body, $headers) = $result;
+
+    if (!$status->isError()) {
+      return $body;
+    }
+
+    if ($status->getStatusCode() === 404) {
+      return null;
+    }
+
+    return parent::didReceiveResult($result);
+  }
+
 }
diff --git a/src/future/aws/PhutilAWSv4Signature.php b/src/future/aws/PhutilAWSv4Signature.php
--- a/src/future/aws/PhutilAWSv4Signature.php
+++ b/src/future/aws/PhutilAWSv4Signature.php
@@ -28,7 +28,7 @@
 
   public function getDate() {
     if ($this->date === null) {
-      $this->date = date('c');
+      $this->date = gmdate('Ymd\THis\Z', time());
     }
     return $this->date;
   }
diff --git a/src/future/aws/management/PhutilAWSManagementWorkflow.php b/src/future/aws/management/PhutilAWSManagementWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSManagementWorkflow.php
@@ -0,0 +1,69 @@
+<?php
+
+abstract class PhutilAWSManagementWorkflow
+  extends PhutilArgumentWorkflow {
+
+  public function isExecutable() {
+    return true;
+  }
+
+  protected function newAWSFuture($template) {
+    $argv = $this->getArgv();
+
+    $access_key = $argv->getArg('access-key');
+    $secret_key = $argv->getArg('secret-key');
+
+    $has_root = (strlen($access_key) || strlen($secret_key));
+    if ($has_root) {
+      if (!strlen($access_key) || !strlen($secret_key)) {
+        throw new PhutilArgumentUsageException(
+          pht(
+            'When specifying AWS credentials with --access-key and '.
+            '--secret-key, you must provide both keys.'));
+      }
+
+      $template->setAccessKey($access_key);
+      $template->setSecretKey(new PhutilOpaqueEnvelope($secret_key));
+    }
+
+    $has_any = ($has_root);
+    if (!$has_any) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'You must specify AWS credentials. Use --access-key and '.
+          '--secret-key to provide root credentials.'));
+    }
+
+    $region = $argv->getArg('region');
+    if (!strlen($region)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'You must specify an AWS region with --region.'));
+    }
+
+    $template->setRegion($region);
+
+    return $template;
+  }
+
+  protected function getAWSArguments() {
+    return array(
+      array(
+        'name' => 'access-key',
+        'param' => 'key',
+        'help' => pht('AWS access key.'),
+      ),
+      array(
+        'name' => 'secret-key',
+        'param' => 'file',
+        'help' => pht('AWS secret key.'),
+      ),
+      array(
+        'name' => 'region',
+        'param' => 'region',
+        'help' => pht('AWS region.'),
+      ),
+    );
+  }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php
@@ -0,0 +1,57 @@
+<?php
+
+final class PhutilAWSS3GetManagementWorkflow
+  extends PhutilAWSS3ManagementWorkflow {
+
+  protected function didConstruct() {
+    $this
+      ->setName('get')
+      ->setExamples(
+        '**get** --key __key__')
+      ->setSynopsis(pht('Download content from S3.'))
+      ->setArguments(
+        array_merge(
+          $this->getAWSArguments(),
+          $this->getAWSS3BucketArguments(),
+          array(
+            array(
+              'name'    => 'key',
+              'param'   => 'key',
+              'help'    => pht('Specify a key to retrieve.'),
+            ),
+          )));
+  }
+
+  public function execute(PhutilArgumentParser $args) {
+    $bucket = $args->getArg('bucket');
+    if (!strlen($bucket)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify an AWS S3 bucket to access with --bucket.'));
+    }
+
+    $endpoint = $args->getArg('endpoint');
+    if (!strlen($endpoint)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify an AWS S3 endpoint with --endpoint.'));
+    }
+
+    $key = $args->getArg('key');
+    if (!strlen($key)) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify an AWS S3 object key to access with --key.'));
+    }
+
+    $future = $this->newAWSFuture(new PhutilAWSS3Future())
+      ->setBucket($bucket)
+      ->setEndpoint($endpoint)
+      ->setParametersForGetObject($key);
+
+    echo $future->resolve();
+
+    return 0;
+  }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php
@@ -0,0 +1,21 @@
+<?php
+
+abstract class PhutilAWSS3ManagementWorkflow
+  extends PhutilAWSManagementWorkflow {
+
+  protected function getAWSS3BucketArguments() {
+    return array(
+      array(
+        'name' => 'bucket',
+        'param' => 'bucket',
+        'help' => pht('Name of the S3 bucket to access.'),
+      ),
+      array(
+        'name' => 'endpoint',
+        'param' => 'endpoint',
+        'help' => pht('Name of the AWS region to access.'),
+      ),
+    );
+  }
+
+}
diff --git a/src/parser/argument/PhutilArgumentParser.php b/src/parser/argument/PhutilArgumentParser.php
--- a/src/parser/argument/PhutilArgumentParser.php
+++ b/src/parser/argument/PhutilArgumentParser.php
@@ -402,7 +402,9 @@
       $this->parse($workflow->getArguments());
     }
 
+
     if ($workflow->isExecutable()) {
+      $workflow->setArgv($this);
       $err = $workflow->execute($this);
       exit($err);
     } else {
diff --git a/src/parser/argument/workflow/PhutilArgumentWorkflow.php b/src/parser/argument/workflow/PhutilArgumentWorkflow.php
--- a/src/parser/argument/workflow/PhutilArgumentWorkflow.php
+++ b/src/parser/argument/workflow/PhutilArgumentWorkflow.php
@@ -80,6 +80,7 @@
   private $specs = array();
   private $examples;
   private $help;
+  private $argv;
 
   final public function __construct() {
     $this->didConstruct();
@@ -154,6 +155,15 @@
     return $this->specs;
   }
 
+  final public function setArgv(PhutilArgumentParser $argv) {
+    $this->argv = $argv;
+    return $this;
+  }
+
+  final public function getArgv() {
+    return $this->argv;
+  }
+
   protected function didConstruct() {
     return null;
   }