Swift, WCF WS2007HttpBinding and NBS Guidance on the Mac.

Background (and disclaimer)

I got my first IBM compatible PC in my early teens. It was a blisteringly fast 386sx running at 25Mhz, with a 40MB HDD all running MS-DOS 6 and Windows 3.1. I continued as a PC user until my 18th birthday back in 1999, when I got an iMac as a present from my parents. For the next few years I was quite a keen Mac user and during University started looking at the various Mac programming languages such REALBasic, Carbon and Objective-C Cocoa.

In 2008, I switched back to PC – mainly because my Core Duo iMac was starting to show it’s age against the new Intel Core i3/i5/i7 processors and it was dirt cheap (in comparison to a new iMac) to custom build a new Windows based Core ix system. I was sad leaving the Mac platform though as both the hardware and software are fantastic (even though the platform is quite closed).

In 2015, I returned to the Mac platform after using several Macs to present the BIM Toolkit. Using the Mac again, even briefly, brought back memories of the platform. I got myself a MacBook Air and very quickly after returning to the platform, got back in to looking at how the development tools had progressed since 2008. The new kid on the block is Swift, which seems to be steadily replacing Objective-C as the language used for iOS and MacApp development.

At NBS, our desktop products are written for Windows. Early versions of NBS Specification Manager were written in Visual C++, and were then ported to .NET 1.0. .NET WinForms is heavily tied to the underlying Windows APIs so isn’t easily portable to other operating systems. However, as a Mac user, I’ve always been keen on trying to do some sort of Mac prototype. I was eager to try to do some kind of MacApp using Swift and thought I could create a simple(ish) NBS Guidance viewer app.

Just before I get too in to the details, it’s important to mention that the work discussed in this post is purely hobby work created in my own time. It’s a proof of concept/training application to learn Swift.

Features/requirements

The application I wanted to create would display the NBS Guidance from NBS Create in a native MacApp. The features I wanted to implement were:

  • Login and take NBS Create license seats to view NBS Guidance
  • Navigate NBS Guidance
  • Search NBS Guidance
  • Print NBS Guidance
  • Open external references (such as British Standards) in the users default web browser.
  • Add, Edit and Delete notes
  • Create Unit tests to automate testing of the application

Challenges

In implementing the above, I encountered a number of problems and at the very least hope that someone will find some of my solutions helpful.

The challenges I faced were:

  • Authenticating an NBS user account against the WS2007HttpBinding of our WCF licensing web service.
  • Embedding a WebKit view within a MacApp
  • Calling JavaScript methods within the WebKitView from Swift
  • Calling Swift methods from the WebKitView

Create Licensing Service

The first hurdle I had to jump was authenticating an NBS user account against our NBS Create Licensing web service. The licensing web service endpoint we need to communicate with uses the WS2007HttpBinding. We use this binding over SSL to provide end-to-end encryption from the client to the server. The users’s username and password are used for authentication and internally verified against our user account database.

The WCF service was created back in 2009 and all requests and responses are sent as SOAP envelops. This makes request and response messages quite verbose. .NET has a nifty feature of building a proxy client based on the WCF service’s web service definition. This wraps up/auto generates a lot of the code  to invoke endpoint methods and authenticate requests. I would have to understand how the proxy does this in order for my Swift project to send the same requests to the service.

It took hours of reading to fathom how to authenticate against a WS2007HttpBinding – to understand the WS_Trust specification and the algorithms used to encrypt and sign messages. I even had to look in the .NET source!

Communicating with the licensing service

Authenticating with a WCF service via a WS2007HTTPBinding takes a number of steps.

Step 1

We need to establish a security context (or a session) with the server. This involves sending an unauthenticated request for a security token to the server with a few bits of key information that will be used to establish end-to-end encryption between the client and the server.

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/SCT</a:Action>
    <a:MessageID>urn:uuid:Client generated GUID</a:MessageID>
    <a:ReplyTo>
        <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo>
    <a:To s:mustUnderstand="1">Service URI</a:To>
    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
      <u:Timestamp u:Id="_0">
        <u:Created>2016-09-10T20:24:34.008Z</u:Created>
        <u:Expires>2016-09-10T20:25:34.008Z</u:Expires>
      </u:Timestamp>
      <o:UsernameToken u:Id="uuid-Client generated GUID-1">
        <o:Username>username</o:Username>
        <o:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">password</o:Password>
        </o:UsernameToken>
    </o:Security>
  </s:Header>
  <s:Body>
    <trust:RequestSecurityToken xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
      <trust:TokenType>http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512/sct</trust:TokenType>
      <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
      <trust:Entropy>
        <trust:BinarySecret u:Id="uuid-Client generated GUID" Type="http://docs.oasis-open.org/ws-sx/ws-trust/200512/Nonce">Client nonce</trust:BinarySecret>
      </trust:Entropy>
      <trust:KeySize>256</trust:KeySize>
    </trust:RequestSecurityToken>
  </s:Body>
</s:Envelope>

You can see that because we’re using SOAP, the messages are quite verbose and there is quite a lot going on.

  • There are several GUIDs that the client needs to generate – MessageID, UsernameTokenID and BinarySecretID. These are created in Swift as NSUUID
  • Our service uses UsernameToken authentication, so the username and password must be sent in the request. This is why we use SSL, so this data is encrypted.
  • The client must generate Entropy (a number once (cnonce)), the server will response with its Entropy (number once (nonce)) and both nonces will be used to sign subsequent messages sent to the server so that the sever knows it’s a genuine request from our client.
  • The client nonce just is simply a 32 random bytes that is BASE64 encoded. My solution uses the following code to generate a secure random 32 byte array
let s = NSMutableData(length: 32)
SecRandomCopyBytes(kSecRandomDefault, s!.length, UnsafeMutablePointer<UInt8>(s!.mutableBytes))
let nonceString = s!.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))

Step 2

The server will respond with a fairly long message:

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTR/SCT</a:Action>
    <a:RelatesTo>urn:uuid:Message GUID</a:RelatesTo>
    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
      <u:Timestamp u:Id="_0">
        <u:Created>2016-09-10T21:00:31.798Z</u:Created>
        <u:Expires>2016-09-10T21:05:31.798Z</u:Expires>
      </u:Timestamp>
    </o:Security>
  </s:Header>
  <s:Body>
    <trust:RequestSecurityTokenResponseCollection xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
    <trust:RequestSecurityTokenResponse>
      <trust:TokenType>http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512/sct</trust:TokenType>
    <trust:RequestedSecurityToken>
      <sc:SecurityContextToken u:Id="uuid-66e50eda-1209-4c6a-b893-66e0ed15a79f-7681" xmlns:sc="http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512">
        <sc:Identifier>urn:uuid:6fc437e3-e0dd-4847-882c-c31a9948324b</sc:Identifier>
      </sc:SecurityContextToken>
    </trust:RequestedSecurityToken>
    <trust:RequestedAttachedReference>
      <o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
        <o:Reference ValueType="http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512/sct" URI="#uuid-66e50eda-1209-4c6a-b893-66e0ed15a79f-7681"></o:Reference>
      </o:SecurityTokenReference>
      </trust:RequestedAttachedReference>
      <trust:RequestedUnattachedReference>
        <o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
          <o:Reference URI="urn:uuid:6fc437e3-e0dd-4847-882c-c31a9948324b" ValueType="http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512/sct"></o:Reference>
        </o:SecurityTokenReference>
      </trust:RequestedUnattachedReference>
      <trust:RequestedProofToken>
        <trust:ComputedKey>http://docs.oasis-open.org/ws-sx/ws-trust/200512/CK/PSHA1</trust:ComputedKey>
      </trust:RequestedProofToken>
      <trust:Entropy>
        <trust:BinarySecret u:Id="uuid-66e50eda-1209-4c6a-b893-66e0ed15a79f-7682" Type="http://docs.oasis-open.org/ws-sx/ws-trust/200512/Nonce">Server Nonce</trust:BinarySecret>
      </trust:Entropy>
      <trust:Lifetime>
        <u:Created>2016-09-10T21:00:31.798Z</u:Created>
        <u:Expires>2016-09-11T12:00:31.798Z</u:Expires>
      </trust:Lifetime>
      <trust:KeySize>256</trust:KeySize>
      </trust:RequestSecurityTokenResponse>
    </trust:RequestSecurityTokenResponseCollection>
  </s:Body>
</s:Envelope>

The 2 bits of information we really need from the servers response are:

  • The sc:SecurityContextToken element, this is the security context that the server has established.
  • The servers nonce (trust:BinarySecret). We need our client nice and the server nonce to compute a 256 bit combined key. Only our client and the server know these nonce values.

Step 3

We now have enough information to invoke an authenticated request to a method of the licensing service.

The (lengthy) request we will send will look something like this:

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
 <s:Header>
   <a:Action s:mustUnderstand="1">http://tempuri.org/PivotalLicensingWebService/VerifyUserAccount</a:Action>
   <a:MessageID>urn:uuid:MessageID GUID</a:MessageID>
   <a:ReplyTo>
     <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
   </a:ReplyTo>
   <a:To s:mustUnderstand="1">NBS licensing service URI</a:To>
   <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
     <u:Timestamp xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" u:Id="_0">
       <u:Created>2016-09-13T19:03:50.450Z</u:Created>
       <u:Expires>2016-09-13T19:33:50.450Z</u:Expires>
     </u:Timestamp>
     <sc:SecurityContextToken u:Id="uuid-SecurityContextToken Id GUID" xmlns:sc="http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512">
       <sc:Identifier>urn:uuid:SecurityContextToken Identifier GUID</sc:Identifier>
     </sc:SecurityContextToken>
     <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
       <SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
         <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></CanonicalizationMethod>
         <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"></SignatureMethod>
         <Reference URI="#_0">
           <Transforms>
             <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></Transform>
           </Transforms>
           <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod>
           <DigestValue>Timestamp SHA1</DigestValue>
         </Reference>
       </SignedInfo>
       <SignatureValue>SignedInfo HMACSHA1</SignatureValue>
       <KeyInfo>
         <o:SecurityTokenReference>
           <o:Reference URI="#uuid-885b42d2-70d2-44a5-8bcd-3f2083d8113f-85591" ValueType="http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512/sct"/>
         </o:SecurityTokenReference>
       </KeyInfo>
     </Signature>
   </o:Security>
 </s:Header>
 <s:Body>
   <VerifyUserAccount xmlns="http://tempuri.org/">
     <Username>NBS user acount username</Username>
     <Password>NBS user account password</Password>
   </VerifyUserAccount>
 </s:Body>
</s:Envelope>

One thing to point out that is *really* important, is that the XML we send to the server, or anything we sign (more on this below) MUST be in Canonical XML form so that the client and server are working on the exact same sequence of XML.

Before we can send the request we need to do a little bit of work. Firstly, we need to create some XML with a timestamp in it:

<u:Timestamp xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" u:Id="_0">
  <u:Created>2016-09-13T19:03:50.450Z</u:Created>
  <u:Expires>2016-09-13T19:33:50.450Z</u:Expires>
</u:Timestamp>

We then need to create an SHA1 hash of the timestamp XML. This hash has to be added to the SOAP message we send to the server – in the value of the <SignedInfo><DigestValue> element. Finally we need to sign the <SignedInfo> element using a HMAC-SHA1 hash. This is a type of message authentication that involves a cryptographic hash with a secret key. The secret key is computed using a PSHA1 hash algorithm that takes the client nonce and server nonce that were previously exchanged.

Phew, that all sounds a bit complicated – and to be honest I’m not at all an expert in this area. But it does make sense that the client and server have the same knowledge to generate the same key – and use that to generate the same hash on the exact same XML. In this way the client and the server know that the message really did come from the client/server.

I create a little wrapper class for the Cryptographic functions I required:

public class Crypto : NSObject {
    public static func sha1(data: String) -> String {
        let data = data.dataUsingEncoding(NSUTF8StringEncoding)!
        var digest = [UInt8](count:Int(CC_SHA1_DIGEST_LENGTH), repeatedValue: 0)

        CC_SHA1(data.bytes, CC_LONG(data.length), &digest)

        let result = NSData(bytes: digest, length: Int(CC_SHA1_DIGEST_LENGTH))
        return result.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
    }

    // The symmetric key generation chosen is
    // http://schemas.xmlsoap.org/ws/2005/02/trust/CK/PSHA1
    // which per the WS-Trust specification is defined as follows:
    //
    //   The key is computed using P_SHA1
    //   from the TLS specification to generate
    //   a bit stream using entropy from both
    //   sides. The exact form is:
    //
    //   key = P_SHA1 (EntREQ, EntRES)
    //
    // where P_SHA1 is defined per http://www.ietf.org/rfc/rfc2246.txt
    // and EntREQ is the entropy supplied by the requestor and EntRES
    // is the entrophy supplied by the issuer.
    //
    // From http://www.faqs.org/rfcs/rfc2246.html:
    //
    // 8<------------------------------------------------------------>8
    // First, we define a data expansion function, P_hash(secret, data)
    // which uses a single hash function to expand a secret and seed
    // into an arbitrary quantity of output:
    //
    // P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
    //                        HMAC_hash(secret, A(2) + seed) +
    //                        HMAC_hash(secret, A(3) + seed) + ...
    //
    // Where + indicates concatenation.
    //
    // A() is defined as:
    //   A(0) = seed
    //   A(i) = HMAC_hash(secret, A(i-1))
    //
    // P_hash can be iterated as many times as is necessary to produce
    // the required quantity of data. For example, if P_SHA-1 was
    // being used to create 64 bytes of data, it would have to be
    // iterated 4 times (through A(4)), creating 80 bytes of output
    // data; the last 16 bytes of the final iteration would then be
    // discarded, leaving 64 bytes of output data.
    // 8<------------------------------------------------------------>8
    public static func computeCombinedKey(reqEntropy: String, resEntropy: String, keySizeInBits: Int = 256) -> NSData {
        let requestorEntropy = NSData(base64EncodedString: reqEntropy, options: NSDataBase64DecodingOptions.init(rawValue: 0))
        let issuerEntropy = NSData(base64EncodedString: resEntropy, options: NSDataBase64DecodingOptions.init(rawValue: 0))

        let keySizeInBytes = keySizeInBits / 8;
        let key = NSMutableData(capacity: keySizeInBytes)

        let khaKey: NSData = requestorEntropy!

        // A(0), the 'seed'.
        var a: NSData = issuerEntropy!
        // Buffer for A(i) + seed
        var b: NSMutableData = NSMutableData(capacity: 160 / 8 + a.length)!
        var result = NSData()

        var i = 0
        while i < keySizeInBytes {
            // Calculate A(i+1).
            a = hmacSha1(a, key: khaKey)
            
            // Calculate A(i) + seed
            b = NSMutableData(capacity: 160 / 8 + a.length)!
            b.appendData(a)
            b.appendData(issuerEntropy!)
           
            result = NSData()
            result = hmacSha1(b, key: khaKey)
            
            for j in 0 ..< result.length {
                if i < keySizeInBytes {
                    i += 1
                    key!.appendData(result.subdataWithRange(NSRange.init(location: j, length: 1)))
                } else {
                    break;
                }
            }
        }
        
        return key!
    }
    
    public static func hmacSha1(data: NSData, key: NSData) -> NSData {
        let digestLen = CryptoAlgorithm.SHA1.digestLength
        let result = UnsafeMutablePointer<UInt8>.alloc(digestLen)
        
        let dataUnsafe = UnsafePointer<UInt8>(data.bytes)
        let keyUnsafe = UnsafePointer<UInt8>(key.bytes)
        
        CCHmac(CryptoAlgorithm.SHA1.HMACAlgorithm, keyUnsafe, key.length, dataUnsafe, data.length, result)
        
        let digest = NSData(bytes: result, length: digestLen)
        result.dealloc(digestLen)
        
        return digest
    }
}

The SHA1 hashes use the CommonCrypto built in to MacOS X (10.5 and later). The PSHA1 hash was a little bit tricker as I wasn’t able to find a Swift equivalent that generated the same keys as .NET. For the solution, I had to look a the .NET source and translate from C# to Swift.

I would have also been fighting a losing battle if I hadn’t enabled WCF tracing to output digests that were computed by the server (and Service Trace Viewer). I took example digests from the trace log, and created Unit tests to ensure I was calculating the exact same signatures.

After several days of reading specs, blog posts and tearing my hair out I was finally successful in sending an authenticated message to the Licensing service.

Displaying NBS Guidance

Once I was able to make authenticated requests to the NBS licensing service, I was able to take seats and obtain tokens to display the NBS Guidance. I thought it would be quite nice for this sample app to have the capability to read, edit and add practice notes to the NBS Guidance (a feature of NBS Create).

There was quite a lot more work that went in to this, but this blog post is quite long at this point, so will have to wait for another day. In the meantime though, here are lots of screenshots of the capabilities that were implemented.

guidance-login

 

Navigate NBS Guidance pages

Link to external citations such as British Standards and Building Regulations

View and zoom in to NBS Guidance graphics

Add practice notes

 

guidance-print

Print guidance

guidance-search

Search the guidance