2

I'm working on a Roku channel, and we want to have files hosted in an AWS S3 bucket, with CloudFront to distribute the content. Before considering security, it worked fine. However, now that I'm trying to be mindful of security issues, I'm experiencing problems. I have the S3 bucket as a private bucket (no public access to anything in it) and I created an origin access identity for the CloudFront distribution so that it can access the content in the bucket.

The problem is, I am unable to create expiring signed URLs (or cookies either) within the channel brightscript code to get access to the content. I can create a signed URL using Amazon's perl script via the command line, and if I copy/paste the signature portion of the link it gives me into the signature portion of the URL I create in brightscript, (replacing the signature) it works. Of course, that's because everything else about the URLs is identical, so once I replace the signature I just have the other URL. So I know (at least I think I can safely say) that the problem is with the signature. I follow the steps indicated in AWS' documentation, but it always returns with an "Access Denied" error message.

The only part of the signing process that I have left out is the base 64 encoding. I have tried base 64 encoding the signature the brightscript creates using this site and updating the URL and trying it, but still no luck. I'm feeling like it has something to do with how brightscript hashes or signs things. I saw in a Stack Overflow post that openssl (which is what the perl script uses to hash/sign) also encodes into ASN.1 before signing... I've tried tinkering with that as well to see if I could get it to work including that step, but no luck there either.

Maybe I'm not doing it right, or maybe that's not the problem. I know some people use S3 and CloudFront to host content for Roku channels, so I don't know why it shouldn't work. Hopefully someone out there can shed some light... If someone knows a solution I would be thrilled to hear it!

Edit: I realized that the way that I was converting from byteArray to string was WAY WRONG! I changed it from this:

  //To convert from byteArray to string
  signatureString = ""
  for each byte in signature
      signatureString = signatureString + stri(byte)
  end for

to this:

  sigString = signature.ToAsciiString()
  print "sigString: ";sigString
  signatureString = signature.ToBase64String()

Unfortunately, I'm still getting access denied. However, at least now my URLs look like the URLs that the perl script creates -- before the signature portion was just a bunch of numbers. Plus, I am now base 64 encoding the signature as well. I feel like I might be getting closer! :)

Edit 2: I have a topic open on the Roku forums with a little bit of activity: https://forums.roku.com/viewtopic.php?t=54797 I discovered that the policy string wasn't right - it had "Condition" before "Resource" (as a result of JSON parsing it from an roAssociativeArray). I was able to get the correct policy string but still I'm getting access denied.

Here's the code I use to create a signed URL:

  readInternet = createObject("roUrlTransfer")


  policy = { "Statement": [
                  {
                     "Resource":"http://XXXXXXXXXXXXX.cloudfront.net/icon_focus_sd.png",
                     "Condition": {
                         "DateLessThan": {
                               "AWS:EpochTime": 1561230905
                          }
                      }
                   }
  ]
  }

  policyString = FormatJson(policy)
  print "policyString: ";policyString

  //UPDATE: correct policy string now:
  policyString = "{" + Chr(34) + "Statement" + Chr(34) + ":[{" + Chr(34) + "Resource" + Chr(34) + ":" + Chr(34) + "http://d1uuhuldzrqhow.cloudfront.net/icon_focus_sd.png" + Chr(34) + "," + Chr(34) + "Condition" + Chr(34) + ":{" + Chr(34) + "DateLessThan" + Chr(34) + ":{" + Chr(34) + "AWS:EpochTime" + Chr(34) + ":1561230905}}}]}"
  ba.FromAsciiString(policyString)
  print "New policy string: ";policyString

  ba = CreateObject("roByteArray")
  ba.FromAsciiString(policyString)

  digest = CreateObject("roEVPDigest")
  digest.Setup("sha1")
  hashString = digest.Process(ba)
  print "hashString: ";hashString

  hashBA = CreateObject("roByteArray")
  hashBA.FromHexString(hashString)

  rsa = CreateObject("roRSA")  
  rsa.setPrivateKey("pkg:/components/key/privateKey.pem")
  rsa.SetDigestAlgorithm("sha1")

  signature = rsa.Sign(hashBA)

  //EDIT! The following 3 lines are a big development!
  sigString = signature.ToAsciiString()
  print "sigString: ";sigString
  signatureString = signature.ToBase64String()

  //To convert from byteArray to string --Commented this part out as it was WRONG!!!
  //signatureString = ""
  //for each byte in signature
  //    signatureString = signatureString + stri(byte)
  //end for

  print "Signature: ";signature
  print "SignatureString: ";signatureString

  baseURL = policy.statement[0].resource
  print "BaseURL: ";baseURL
  fixedSignatureString = signatureString.replace(" ", "").replace("=", "_").replace("/", "~").replace("+", "-")


  dateKeys = policy.statement[0].condition.datelessthan.Keys()
  print"dateKeys: ";dateKeys
  print"dateKey: ";dateKeys[0]

  epochTime = policy.statement[0].condition.dateLessThan.Lookup(dateKeys[0])

  finalURL = baseURL + "?Expires=" + stri(epochTime).Replace(" ","") + "&Signature=" + fixedSignatureString + "&Key-Pair-Id=APKXXXXXXXXXXVWQ"
  print "finalURL: ";finalURL


  readInternet.setUrl(finalURL)

  readInternet.SetCertificatesFile("common:/certs/ca-bundle.crt")
  readInternet.AddHeader("X-Roku-Reserved-Dev-Id", "")
  readInternet.InitClientCertificates()

  readInternet.RetainBodyOnError(true)
  response = ParseJson(readInternet.GetToString())

  print "response:" response

I also tried using this to create signed cookies instead:

cookies = [
   {Name:"CloudFront-Policy",Value:policy,Path:"/", Domain:"XXXXXXXXXX.cloudfront.net"},
   {Name:"CloudFront-Expires",Value:"1561230905",Path:"/", Domain:"XXXXXXXXXX.cloudfront.net"},
   {Name:"CloudFront-Signature",Value:fixedSignatureString,Path:"/", Domain:"XXXXXXXXXX.cloudfront.net"},
   {Name:"CloudFront-Key-Pair-Id",Value:"APKAXXXXXXXXXXAVWQ", Path:"/", Domain:"XXXXXXXXXX.cloudfront.net"}
]

readInternet.EnableCookies()
readInternet.AddCookies(cookies)

I also tried the following instead of the previous method of adding cookies:

expires = "CloudFront-Expires=1561146117" //I have been careful to make sure the expire times are still good
sig = "CloudFront-Signature=" + fixedSignatureString
pairid = "CloudFront-Key-Pair-Id=APKAXXXXXXXXXXVWQ"
readInternet.AddHeader("Cookie",expires + "; " + sig + "; " + pairid)

bmbudai
  • 31
  • 4
  • I same issue with SSL Certificate for IIS Hosting. But Now I think something different and my code shifted in self-hosting. so, Not Required any signature. – Nikunj Chaklasiya Jun 21 '19 at 07:59
  • if you're using the same signed url for multiple urls, you need to use custom policy and use resources as https://example.com/* and create a signed url for this and this will work for all the URIs. – James Dean Jun 21 '19 at 08:51
  • @JamesDean Right now I'm just testing so I only have one file that I'm trying to access. The resources value in the policy is http://XXXXXXXX.cloudfront.net/icon_focus_sd.png. Are you saying that I should leave off the name of the specific file I'm trying to access in the policy? I'm not planning to use the same URL for multiple files, my plan was to create a new signed URL each time I need to access a file and have the URLs expire as quickly as possible without causing problems. – bmbudai Jun 21 '19 at 15:27
  • No, if you're doing it right, resource value should be defined completely when creating signed url (I was talking canned vs custom policy), Is this access denied error from CloudFront or S3 ? You'll see xml with two long request ids if it's from S3, otherwise a simple Access denied error in html if it's CloudFront. – James Dean Jun 21 '19 at 17:31
  • Should be from CloudFront: AccessDeniedAccess denied – bmbudai Jun 21 '19 at 17:47

1 Answers1

1

I finally got this ironed out! I started just going through (again, but more carefully) and seeing what was the same and what was different when creating a signed URL with BrightScript and creating one with the perl script. As I added to the original question post, I was changing the signature from a byteArray to a string in the wrong way. Fixing that was the first step. Once I figured that out, I was noticing that when I signed the URL from the command line (not from the perl script) it was coming up with the same (non-working) signature that I was getting from the BrightScript. I then found out that I was using the wrong private key! (I know - brilliant, right?) Switched to using the correct key and it all works now! I just need to make it all dynamic so that it will work for all of my files and dynamically set the expiry time.

So here's a summary of what I learned:

  • Make sure that the policy string is correct - the order matters! Using formatJSON(roAssociativeArray) will break it because it puts it in alphabetical order.
  • Stringifying every byte in an roByteArray DOES NOT convert it to a string correctly!! Instead, use .ToAsciiString() or .ToBase64String(), depending on what you need.
  • Using the correct key turns out to be important - who knew. *insert facepalm*

Here is my code now that it is working:

    readInternet = createObject("roUrlTransfer")

    ba = CreateObject("roByteArray")
    policyString = "{" + Chr(34) + "Statement" + Chr(34) + ":[{" + Chr(34) + "Resource" + Chr(34) + ":" + Chr(34) + "http://XXXXXXXXX.cloudfront.net/icon_focus_sd.png" + Chr(34) + "," + Chr(34) + "Condition" + Chr(34) + ":{" + Chr(34) + "DateLessThan" + Chr(34) + ":{" + Chr(34) + "AWS:EpochTime" + Chr(34) + ":1561230905}}}]}"
    ba.FromAsciiString(policyString)
    print "New policy string: ";policyString

    digest = CreateObject("roEVPDigest")
    digest.Setup("sha1")

    hashString = digest.Process(ba)
    print "hashString: ";hashString
    hashBA = CreateObject("roByteArray")
    hashBA.FromHexString(hashString)

    rsa = CreateObject("roRSA")
    rsa.setPrivateKey("pkg:/components/key/privateKey.pem")
    rsa.SetDigestAlgorithm("sha1")

    signature = rsa.Sign(hashBA)
    signatureString = signature.ToBase64String()

    print "Signature: ";signature
    print "SignatureString: ";signatureString

    baseURL = "http://XXXXXXXXXXX.cloudfront.net/icon_focus_sd.png"
    print "BaseURL: ";baseURL
    fixedSignatureString = signatureString.replace(" ", "").replace("=", "_").replace("/", "~").replace("+", "-")

    epochTime = 1561230905

    finalURL = baseURL + "?Expires=" + stri(epochTime).Replace(" ","") + "&Signature=" + fixedSignatureString + "&Key-Pair-Id=APKAXXXXXXXXXXXVWQ"
    print "finalURL: ";finalURL

    readInternet.setUrl(finalURL)

    readInternet.SetCertificatesFile("common:/certs/ca-bundle.crt")
    readInternet.AddHeader("X-Roku-Reserved-Dev-Id", "")
    readInternet.InitClientCertificates()

    readInternet.RetainBodyOnError(true)
    response = ParseJson(readInternet.GetToString())


Thanks to everyone who participated/gave suggestions. I appreciate the support.

bmbudai
  • 31
  • 4
  • Note that the order and format in the JSON string is only important if you are generating a canned policy, because what you're actually doing is building a *canonical* policy statement, where ordering is standardized, as well as whitespace. If you use a custom policy, this isn't necessary, but of course the signed URL is much longer. – Michael - sqlbot Jun 22 '19 at 00:37