phpaws-sdkamazon-cloudfrontaws-access-policy

CloudFront "MalformedPolicy" error with signed URLs


I need to create signed CloudFront URLs with a custom policy using PHP, but no matter what I do apparently my policy is "malformed". Here is an example policy generated in the function:

{"Statement":{"Resource":"https://d15xojelh58w5d.cloudfront.net/memo/kwz/cvyhkfdqn5oz0z1dz5at4z4s1jsn.kwz","Condition":{"DateLessThan":{"AWS:EpochTime":1490463203},"IpAddress":{"AWS:SourceIp":"1.2.3.4/32"}}}}

Generated URL:

https://d15xojelh58w5d.cloudfront.net/memo/kwz/cvyhkfdqn5oz0z1dz5at4z4s1jsn.kwz?Policy=eyJTdGF0ZW1lbnQiOnsiUmVzb3VyY2UiOiJodHRwczovL2QxNXhvamVsaDU4dzVkLmNsb3VkZnJvbnQubmV0L21lbW8va3d6L2N2eWhrZmRxbjVvejB6MWR6NWF0NHo0czFqc24ua3d6IiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNDkwNDYzMjAzfSwiSXBBZGRyZXNzIjp7IkFXUzpTb3VyY2VJcCI6IjEuMi4zLjQvMzIifX19fQ__&Signature=MmBPtpipFLuNwaPliGLJajG4gJ7INwD0ptFdxPFYQP9CT-luq6W0SrAs9O9CqbJPHoukXwDzG~c88Rr5I2I9KP5QwD8MHpogGh~3SM3gBYm8ao0Zm7a5C9tWnBVRCtzuGrCrFstK-qLswWmqo6tNiOynSuFpvm9uDe3C8oWE2RzSZavEXoL35D3F8y98NeM0aOJe37EeSpdz3lrZZxei2TugoO-OmnApXa2YYJR2HiQ2l0t8paxcb3xyhCK1c1AR51uOpWLm63k~d0eNZJGo3x0Y6bx0GBqafdvV6jiUv6PbhiMC1ZcTxGnZhLmsz3~ONsEvaR1jyyOPt6y9Nos8yA__&Key-Pair-Id=APKAJ6RV6ACUX5M5IAOQ

Code:

function cloudfront_sign($url, $expiry = null, $ipLock = true) {
    $policy = array(
        'Statement' => array(
            'Resource' => $url,
            'Condition' => array(),
        ),
    );

    if(!$expiry || $expiry <= time()) $expiry = 2147483647; // CloudFront *requires* an expiry date, so set to 03:14:07 UTC on Tuesday, 19 January 2038 if one is not provided
    $policy['Statement']['Condition']['DateLessThan'] = array('AWS:EpochTime' => $expiry);
    if($ipLock) $policy['Statement']['Condition']['IpAddress'] = array('AWS:SourceIp' => $_SERVER['REMOTE_ADDR'].'/32');

    $signer = new Aws\CloudFront\UrlSigner($_config['keyID'], $_config['keyPath']);

    $jsonPolicy = json_encode($policy, JSON_UNESCAPED_SLASHES);

    $url = $signer->getSignedUrl($url, null, $jsonPolicy);

    return $url;
}
$url = kaeru_cloudfront_sign('https://d15xojelh58w5d.cloudfront.net/memo/kwz/cvyhkfdqn5oz0z1dz5at4z4s1jsn.kwz', 1490463203);

Solution

  • I can tell you exactly what's going on, but I can't tell you why, unless it's a bug in the version of the SDK you are (apparently) using.

    The policy document is indeed malformed, and the signature is, too.

    And it does not appear that you are actually doing anything wrong.

    I assume you are familiar with base64, where 8 bit data is expanded to 6 bits per octet, to allow binary data to be carried over transports that are not necessarily 8-bit clean by using 64 symbols (where 64 is 2^6, the number of discrete values in 6 bits).

    0-9 and A-Z and a-z make up 10 + 26 + 26 = 62 of the necessary symbols, then there's + and / to bring the total symbols to 64, but since there may be an octet of output at the end with only 2 or 4 bits of input coded into it, a 65th symbol = for "padding" indicates that there are unused bits in the prior symbol that do not represent input data. Thus any base64 representation always ends with 0, 1, or 2 = symbols. For this reason, a base64 encoded value can never have = anywhere except at the end. This turns out to be important, below.

    The choice of the symbols + / = is terrible for a URL, because of ambiguity caused by so many user agents (browsers and HTTP client libraries) handling them incorrectly when it comes to url-escaping (also called url-encoding or percent-encoding).

    The + is sometimes considered equivalent to %20 (space), other times it's escaped as %2B... the = is used to delimit the fields in the query string, so some user agents escape it as %3D... and sometimes the / might be escaped as %2F... all of which leads to an interoperability nightmare.

    (Even S3 itself has at least bug related to incorrect url-escaping, which has been in place too long for it to be fixed, now, because it would break all the code that was written to anticipate the incorrect behavior by S3. But this is unrelated to the problem at hand.)

    CloudFront's designers cleverly worked around this.

    CloudFront transliterates the three potentially problematic characters as follows:

    + => -
    / => _
    = => ~
    

    This works out because the characters - _ ~ are not nearly as error prone in URLs.

    But somehow, in your code, this translation is wrong.

    &Signature=...~ONsEvaR1jyyOPt6y9Nos8yA__&Key-...
    

    This is definitely wrong. The ~ in the middle of the signature cannot possibly be valid. As noted above, ~ is = and that can only be valid at the end of a base64 encoding. It implies that the __ at the end of the signature and policy are also wrong, and should in fact be ~~.

    ?Policy=ey...X19fQ__&Signature=
    

    You can partially confirm this by swapping them out. Change the __ at the end of the policy to ~~ and you will find that you no longer see the Malformed Policy error, because that seems to be the only problem with the policy.

    Unfortunately, since the policy doesn't contain any ~ or -, it isn't possible to come to a conclusion about which character substitutions are necessary to make the signature valid -- not that this is the correct solution, but it should actually work. The problem is, we don't know whether only ~ and _ are transposed with each other, or if all three (including -) are incorrect.

    But this definitely appears to be a problem with the actual code that is generating the final URL, not a problem with the JSON policy document you are supplying to it.