Authentication for scripts in Second Life & the web servers they talk to

by Jean Zee (a.k.a. CmpZ)

created Monday, 2021 September 20
updated Wednesday, 2021 October 6


What is this?

There are ad hoc schemes for web servers to determine that messages they receive originate from intended objects within Second Life.[00]

When it comes to authentication & validation, ad hoc is dangerous, so here's a technique for authentication between LSL scripts & external web servers. It builds on a trustworthy standard called HMAC, but I've modified it to make it easier to use in LSL.

TL, DR: Jump to the All the code section.

Your use case

It doesn't take much effort to extend the use case to allow the LSL script to verify the reply it receives from the web server. I'll leave that to you.

The authentication solution

We'll adapt the HMAC algorithm for use with LSL.[02]

HMAC is defined in bit-twiddling terms, but LSL doesn't do bits all that well. It's better with strings. And the llSHA1String function that the Lindens give us operates on strings of text, even includes warnings about arbitrary binary values. So we need to adapt the HMAC algorithm into something appropriate for LSL. (See “Adapting HMAC for LSL”.) I called the result FakeMAC.

Here's how you use FakeMAC:

What you do in your LSL script

Let's say that your LSL script sends an HTTP request to your web server, sort of like this...

      string
      Create_Request_Body() {
          return some string;
      }

      string THE_URL = "http://example.com/whatever/goop/goes/here";

      // ***
      // This is your original Send_To_Server function
      // **
      Send_To_Server() {
          list parameters;
          string body;

          parameters = [ HTTP_METHOD, "POST" ];
          body = Create_Message_Body();
          llHTTPRequest( THE_URL, parameters, body );
      }
    

To use FakeMAC, you first create a secret that's shared between your script & your web server...

      // The web server knows this same secret.
      string SHARED_SECRET = "j$rA+KiB7Rzad7^&=G*Yo_QN";
    

You copy the Fake_MAC function from fakemac.lsl into your script.[03]

      string
      Fake_MAC( string secret, string message ) {
          string o_pad;
          string i_pad;

          o_pad = llSHA1String( secret + "ooo" );
          i_pad = llSHA1String( secret + "iii" );
          return llSHA1String( o_pad + llSHA1String( i_pad + message ) );
      }
    

Modify your Send_To_Server function so that, after constructing the main body, it calls Fake_MAC to obtain a Message Authentication Code (MAC). And append that MAC to the body.

      // ***
      // This is your NEW Send_To_Server function
      // **
      Send_To_Server() {
          list parameters;
          string real_message;
          string b64;
          string body;

          parameters = [ HTTP_METHOD, "POST" ];
          real_message = Create_Message_Body();
          b64 = llStringToBase64( real_message );
          body += "\n" + Fake_MAC( SHARED_SECRET, b64 );
          llHTTPRequest( THE_URL, parameters, body );
      }
    

(Q. Why call llStringToBase64? A. See the Protips section for the answer.)

And now you need to make changes on your server.

What you do on your web server

Your server receives the message. Before FakeMAC, your server just received the message (the only part), did some ad hoc verification, then did the real work.

When using FakeMAC, your server receives both the message & the FakeMAC code. Here's what the server does now (pseudocode)...

  1. Receive both the real message & the received code.
  2. Use the real message & the shared secret (which the server already knows) to calculate actual code = Fake_MAC( SHARED_SECRET, real message).
  3. If the actual code differs from the received code, reject the message because there's been tampering or forgery.
  4. Otherwise, the codes match, the message is authentic, & you can proceed with the real work.

Notice that when your server has a reply ready, it can package that with a FakeMAC code so that your LSL script can authenticate it.

All the code

I created several prototype implementations. Pick your favorite language.

Inside Second Life, you'll of course need fakemac.lsl.

Those are stand-alone, proof-of-concept programs. You'll need to copy the Fake_MAC function from one of them into your server, then do some work to call it.

Protips!

Protip 0. Where do I put the secret?

This turns out to be an interesting question, not always a simple answer. I'll write about it in another file (exn6p.html).

Protip 1. Good formats for sending message + code to the server

In my example, I've packaged the real messge with the code as two lines. I did that because it's easy for me to parse on my server.

When you package them together, pick a format that's easy to parse on your own server.

These days, JSON is often a good choice. (Maybe you already package your real message in JSON.) If your real message is already a JSON list or object, I caution against simply adding the code as a field to that! The reason is that the SHA1 hash (or any other hash algorithm we use) is sensitive to different amounts of space characters, tabs, & end-of-line sequences. (Remember that end-of-line sequences sometimes, annoyingly, differ from host to host or when sent over the wire.) If you simply add the code as an element to your existing real message, then you'll need to remove that code in a way that exactly (octet-for-octet) recovers the message on the sender's side before you calculate the actual code. It can be done, but in my experience, it's more frustrating than packaging an unaltered real message into an outer message that contains it & the code.

That leads to protip 2! (Keep reading.)

Protip 2. Base64 the real message

The SHA1 algorithm (& all hash algorithms) are sensitive to any change in the values they hash. For example, “hello” followed by an ASCII line feed (#x0A) gets a different value than “hello” followed by a carriate return + line feed (#x0D #x0A). Check it out:

In addition, the llSHA1String function's documentation warns of “zero-byte value into this function, nor any byte value from 128-255”.[See “Caveats” in 01]

To protect against bugs caused by these realities, I recommend Base64-encoding your real message, then calculating the FakeMAC for that. Combine those into an outer message & send it. So you are sending a base64-encoded real message plus the code (which is in base 16, so also safe).

Protip 3: Debugging

When you are figuring out why your server calculates a different code than does your LSL script, remember to print out every octet in the message before you send it & after you receive it, even the end-of-lines.

Protip 4: Timestamps & nonces

Include a timestamp in your real message. Your receiver can use it to verify that the message isn't too old (or isn't too far in the future). This protects against a monster-in-the-middle (MitM) that delayed the message for a long time.

The timestamp must be inside your real message that you then give to FakeMAC. You can't rely on the Date in the HTTP headers because the MitM could hold the entire message for a while, then forward it but with a new Date header.

It's also a little safer to include a nonce. This ensures that every message has a unique real message even if you send the same values so quickly that the timestamp doesn't change. The nonce can be as simple as a value you get from llFrand.

How we get FakeMAC from HMAC (& what HMAC is in the first place)

HMAC is an acronym for Hashed Message Authentication Code. It allows two entities to authenticate each other's messages when they share a secret.[02]

Here's how I derived FakeMAC for LSL from HMAC.

For the inner padded key and outer padded key in FakeMAC, I appended iii and ooo. Those values aren't critical. You could replace them with other sequences.

How secure is it?

HMAC was created by cryptographic experts, which I'm not. I would not be surprised if, by creating FakeMAC, I've reduced the trust that can be placed in HMAC.

On the other hand, I suspect that very few people can tamper with a message in a way that FakeMAC can't detect. Cryptographic experts created HMAc, & hopefully a cryptographic expert is required to fool FakeMAC.

And FakeMAC is a lot better than a reverse DNS lookup, a custom HTTP header, including a secret in the message, & most other ad hoc attempts.

Frequently Answered Questions

Q. Is it okay to use SHA1?

The SHA1 cryptographic hash function is old, replaced by SHA2 in 2001 (long time ago), & that has been replaced by SHA3. So SHA1 is soooo yesterday!

LSL gives us a llSHA1String function, no others. And remember that your script is running in an environment that Linden Labs owns & controls, & that environment is running “in the cloud”, which means on servers that yet another company controls. If they want to tamper with your script or watch what it does, you've lost.

The system I'm presenting here should be sufficient against some jerk who sends bogus messages to your server from his own server or from his own LSL scripts.

When LSL offers a llSHA2String or llSHA3String function, we just update this Fake_MAC function to use it. (And then update your server code.)

I am aware of the user-supplied, {free, libre, open} source SHA2 implementation that's available on wiki.secondlife.com. I gave it a try, but after a couple of hours of debugging, it was still producing hash values that differ from what I could get on my server. I concluded I had spent enough time on it & switched to llSHA2String.

Q. Why did you call it FakeMAC?

FakeMAC sounds a sort of like HMAC, but it's not HMAC. That's sort of like how the FakeMAC algorithm is sort of the HMAC algorithm.

I could have called it LSLMAC, but that'd be pretentious; I don't speak for LSL. More importantly, it creates an upcase/downcase problem. Would it be LSLMAC or LslMAC? (There are some questions that mortals should never ask.)

Q. How can I contact you, Jean?

The Second Life forums might be best: 477996-....

Or contact me in-world. That is, contact me in Second Life. My display name is “Jean Zee”, & my username is CmpZ.

Q. You use UPCASE for globals, not just for constants?

Indeed, upcase is already used for constants. I'm trying them out for globals. It's an experiment.

I also notice that, in LSL at least, constants are globals.

Notes

00. Creation Forum > LSL Scripting > LSL HTTP Changes Coming
Posted 2020 September 10 by Oz Linden. 460866-lsl-http-changes-coming
01. llSHA1String
llSHA1String
02. HMAC
HMAC is an acronym for Hashed Message Authentication Code. See HMAC at Wikipedia.
03. copy from fakemac.lsl
Notice that the Fake_MAC code is covered by the Gnu General Public License.

Changes

whenwhowhat
2021-09-24CmpZ The “string key” parameter of the Fake_MAC function in LSL caused an error because key is a type in LSL. So I changed the parameter's name to secret.
2021-10-06CmpZ Minor edits to improve readability. This is a publishable version. Linked to 2L forum.

Jean Zee's 2nd L blog