Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating an AWS Signature v4 signature for uploading to s3 (migration from v2)

I currently have a working implementation that works as follows:

UI select a file => click upload => call to my backend API to request a signature since I don't want to expose my access + secretkey => return the signature + policy => do an upload to s3.

This works fine and dandy for v2.

String base64Policy = (new BASE64Encoder()).encode(policy.toString().getBytes("UTF-8")).replaceAll("\n", "").replaceAll("\r", "");

        Mac hmac = Mac.getInstance("HmacSHA1");
        hmac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1"));
        String signature = (new BASE64Encoder()).encode(hmac.doFinal(base64Policy.getBytes("UTF-8"))).replaceAll("\n", "");

Now I get to the fun bit where my new buckets are in a region where v2 isn't supported.

I was following the AWS documentation but I think I am misunderstanding the payload bit a bit. Do I really need to have my UI pass in a sha256 hash of my whole file? Since that would seem to be a bit of a pain, especially since my files can be > 1 gig.

The code I was attempting to use:

        byte[] signatureKey = getSignatureKey(secretKey, LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")),  bucketRegion, "s3");
        StringBuilder sb = new StringBuilder();
        for (byte b : signatureKey) {
            sb.append(String.format("%02X", b));
        }

private static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
        byte[] kSecret = ("AWS4" + key).getBytes("UTF8");
        byte[] kDate = HmacSHA256(dateStamp, kSecret);
        byte[] kRegion = HmacSHA256(regionName, kDate);
        byte[] kService = HmacSHA256(serviceName, kRegion);
        byte[] kSigning = HmacSHA256("aws4_request", kService);
        return kSigning;
    }



private static byte[] HmacSHA256(String data, byte[] key) throws Exception {
        String algorithm="HmacSHA256";
        Mac mac = Mac.getInstance(algorithm);
        mac.init(new SecretKeySpec(key, algorithm));
        return mac.doFinal(data.getBytes("UTF8"));
    }

But this gives an invalid signature response when I try to use the rest of my code.

Am I derping that hard, and just misunderstanding: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html ?

Any help would be much appreciated since I've been hanging my head against this way too long and I'd prefer not to overhaul too much.

like image 980
Kristof Plennings Avatar asked Mar 03 '18 07:03

Kristof Plennings


People also ask

How do I migrate to signature 4?

To migrate to Signature Version 4, please replace your existing SMTP credentials using the appropriate procedure relative to your setup: If you generated your SMTP credentials using the SES Console, simply create new credentials and replace your existing credentials with the new ones.

How do I validate my AWS signature?

To verify a digital signature, you can use the Verify operation. Specify the same asymmetric KMS key, message, and signing algorithm that were used to produce the signature. You can also verify the digital signature by using the public key of the KMS key outside of AWS KMS.


1 Answers

You can upload a file to S3 by using standard SDK methods without generating a signature, please see the documentation. But if you need a signature for some reason, I think, the simplest way to generate a signature is to use methods from AWS SDK, please see the following class which extends AWS4Signer:

public class AwsAuthUtil extends AWS4Signer {
    private String serviceName;
    private AWSCredentials credentials;
    private String region;

    public AwsAuthUtil(AWSCredentials credentials, String region, String serviceName) {
        this.credentials = credentials;
        this.region = region;
        this.serviceName = serviceName;
    }

    public String getSignature(String policy, LocalDateTime dateTime) {
        try {
            String dateStamp = dateTime.format(ofPattern("yyyyMMdd"));
            return Hex.encodeHexString(hmacSha256(newSigningKey(credentials, dateStamp, region, serviceName), policy));
        } catch (Exception e) {
            throw new RuntimeException("Error", e);
        }
    }

    private byte[] hmacSha256(byte[] key, String data) throws Exception {
        Mac mac = Mac.getInstance(SigningAlgorithm.HmacSHA256.name());
        mac.init(new SecretKeySpec(key, SigningAlgorithm.HmacSHA256.name()));
        return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
    }
}

where AWS4Signer is from

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-s3</artifactId>
    <version>1.11.213</version>
</dependency>

and AWSCredentials can be built as

AWSCredentials awsCredentials = new BasicAWSCredentials(s3AccessKey, s3SecretKey);

Also you should consider http headers when you use multipart data, for example, please see the following method which builds HttpEntity

public HttpEntity buildPostMultipartDataEntity(String objectKey, byte[] data, String signature, LocalDateTime dateTime) {

    String dateTimeStr = dateTime.format(ofPattern("yyyyMMdd'T'HHmmss'Z'"));
    String date = dateTime.format(ofPattern("yyyyMMdd"));

    return MultipartEntityBuilder
        .create()
        .addTextBody("key", objectKey)
        .addTextBody("Policy", policy)
        .addTextBody("X-Amz-Signature", signature)
        .addTextBody("X-Amz-Algorithm", algorithm)
        .addTextBody("X-Amz-Date", dateTimeStr)
        .addTextBody("X-Amz-Credential", String.format("%s/%s/%s/s3/aws4_request", accessKey, date, region))
        .addBinaryBody("file", data)
        .build();
}
like image 167
statut Avatar answered Oct 27 '22 02:10

statut