11

With the very recent Windows Anniversary update, Edge now supports biometric authentication using Windows Hello (cf. https://developer.microsoft.com/en-us/microsoft-edge/platform/documentation/dev-guide/device/web-authentication/ , https://blogs.windows.com/msedgedev/2016/04/12/a-world-without-passwords-windows-hello-in-microsoft-edge/ )

I have some samples in C#, PHP and Node.js, and am trying to make it work in Go.

The following works in JS (I have hardcoded in the challenge and the key):

function parseBase64(s) {
    s = s.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, '');  
    return new Uint8Array(Array.prototype.map.call(atob(s), function (c) { return c.charCodeAt(0) }));  
}

function concatUint8Array(a1,a2) {
    var d = new Uint8Array(a1.length + a2.length);
    d.set(a1);
    d.set(a2,a1.length);
    return d;
}

var credAlgorithm = "RSASSA-PKCS1-v1_5";
var id,authenticatorData,signature,hash;
webauthn.getAssertion("chalenge").then(function(assertion) {
    id = assertion.credential.id;
    authenticatorData = assertion.authenticatorData;
    signature = assertion.signature;
    return crypto.subtle.digest("SHA-256",parseBase64(assertion.clientData));
}).then(function(h) {
    hash = new Uint8Array(h);
    var publicKey = "{\"kty\":\"RSA\",\"alg\":\"RS256\",\"ext\":false,\"n\":\"mEqGJwp0GL1oVwjRikkNfzd-Rkpb7vIbGodwQkTDsZT4_UE02WDaRa-PjxzL4lPZ4rUpV5SqVxM25aEIeGkEOR_8Xoqx7lpNKNOQs3E_o8hGBzQKpGcA7de678LeAUZdJZcnnQxXYjNf8St3aOIay7QrPoK8wQHEvv8Jqg7O1-pKEKCIwSKikCFHTxLhDDRo31KFG4XLWtLllCfEO6vmQTseT-_8OZPBSHOxR9VhIbY7VBhPq-PeAWURn3G52tQX-802waGmKBZ4B87YtEEPxCNbyyvlk8jRKP1KIrI49bgJhAe5Mow3yycQEnGuPDwLzmJ1lU6I4zgkyL1jI3Ghsw\",\"e\":\"AQAB\"}";
    return crypto.subtle.importKey("jwk",JSON.parse(publicKey),credAlgorithm,false,["verify"]);
}).then(function(key) {
    return crypto.subtle.verify({name:credAlgorithm, hash: { name: "SHA-256" }},key,parseBase64(signature),concatUint8Array(parseBase64(authenticatorData),hash));
}).then(function(result) {
    console.log("ID=" + id + "\r\n" + result);
}).catch(function(err) {
    console.log('got err: ', err);
});

In go I have the following code, meant to match the above JS code (req is a struct with strings from a JSON request body):

func webauthnSigninConversion(g string) ([]byte, error) {
    g = strings.Replace(g, "-", "+", -1)
    g = strings.Replace(g, "_", "/", -1)
    switch(len(g) % 4) { // Pad with trailing '='s
    case 0:
        // No pad chars in this case
    case 2:
        // Two pad chars
        g = g + "=="
    case 3:
        // One pad char
        g = g + "=";
    default:
        return nil, fmt.Errorf("invalid string in public key")
    }
    b, err := base64.StdEncoding.DecodeString(g)
    if err != nil {
        return nil, err
    }
    return b, nil
}


clientData, err := webauthnSigninConversion(req.ClientData)
if err != nil {
    return err
}

authenticatorData, err := webauthnSigninConversion(req.AuthenticatorData)
if err != nil {
    return err
}

signature, err := webauthnSigninConversion(req.Signature)
if err != nil {
    return err
}

publicKey := "{\"kty\":\"RSA\",\"alg\":\"RS256\",\"ext\":false,\"n\":\"mEqGJwp0GL1oVwjRikkNfzd-Rkpb7vIbGodwQkTDsZT4_UE02WDaRa-PjxzL4lPZ4rUpV5SqVxM25aEIeGkEOR_8Xoqx7lpNKNOQs3E_o8hGBzQKpGcA7de678LeAUZdJZcnnQxXYjNf8St3aOIay7QrPoK8wQHEvv8Jqg7O1-pKEKCIwSKikCFHTxLhDDRo31KFG4XLWtLllCfEO6vmQTseT-_8OZPBSHOxR9VhIbY7VBhPq-PeAWURn3G52tQX-802waGmKBZ4B87YtEEPxCNbyyvlk8jRKP1KIrI49bgJhAe5Mow3yycQEnGuPDwLzmJ1lU6I4zgkyL1jI3Ghsw\",\"e\":\"AQAB\"}" // this is really from a db, not hardcoded
// load json from public key, extract modulus and public exponent
obj := strings.Replace(publicKey, "\\", "", -1) // remove escapes
var k struct {
    N string `json:"n"`
    E string `json:"e"`
}
if err = json.Unmarshal([]byte(obj), &k); err != nil {
    return err
}
n, err := webauthnSigninConversion(k.N)
if err != nil {
    return err
}
e, err := webauthnSigninConversion(k.E)
if err != nil {
    return err
}
pk := &rsa.PublicKey{
    N: new(big.Int).SetBytes(n), // modulus
    E: int(new(big.Int).SetBytes(e).Uint64()), // public exponent
}
 
hash := sha256.Sum256(clientData)

// Create data buffer to verify signature over
b := append(authenticatorData, hash[:]...)
 
if err = rsa.VerifyPKCS1v15(pk, crypto.SHA256, b, signature); err != nil {
    return err
}

// if no error, signature matches

This code fails with crypto/rsa: input must be hashed message. If I change to using hash[:] instead of b in rsa.VerifyPKCS1v15, it fails with crypto/rsa: verification error. The reason I believe I need to combine authenticatorData and hash is because that is what happens in the C# and PHP sample codes (cf,  https://github.com/adrianba/fido-snippets/blob/master/csharp/app.cshttps://github.com/adrianba/fido-snippets/blob/master/php/fido-authenticator.php ).

Maybe Go does it a different way?

I have printed the byte arrays in JS and Go, and verified that clientData, signatureData, authenticatorData and hash (and the combined array of the latter two) have the exact same values. I have not been able to extract the n and e fields from JS after creating the public key, so there might be a problem in how I create the public key.

yngling
  • 1,376
  • 2
  • 22
  • 34
  • if someone with 1500+ rep could add a tag called "webauthn" that would be great. (It is the name of the standard, cf. https://www.w3.org/Webauthn/ ) – yngling Aug 14 '16 at 08:27

1 Answers1

2

I'm not a crypto expert but I have some experience in Go, including verifying signatures that were signed with PHP. So, assuming the compared byte values are the same I would say that Your problem is probably the public key creation. I would suggest to try my solution of creating public keys from modulus and exponent with this function:

func CreatePublicKey(nStr, eStr string)(pubKey *rsa.PublicKey, err error){

    decN, err := base64.StdEncoding.DecodeString(nStr)
    n := big.NewInt(0)
    n.SetBytes(decN)

    decE, err := base64.StdEncoding.DecodeString(eStr)
    if err != nil {
        fmt.Println(err)
        return nil, err
    }
    var eBytes []byte
    if len(decE) < 8 {
        eBytes = make([]byte, 8-len(decE), 8)
        eBytes = append(eBytes, decE...)
    } else {
        eBytes = decE
    }
    eReader := bytes.NewReader(eBytes)
    var e uint64
    err = binary.Read(eReader, binary.BigEndian, &e)
    if err != nil {
        fmt.Println(err)
        return nil, err
    }
    pKey := rsa.PublicKey{N: n, E: int(e)}
    return &pKey, nil
}

I compared my public key and Yours (Playground), and they have different values. Could You please give me feedback of the solution I suggested with Your code if it's working?

Edit 1: URLEncoding example Playground 2

Edit 2: This is how I verify the signature:

hasher := sha256.New()
hasher.Write([]byte(data))
err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hasher.Sum(nil), signature)

So the 'data' variable in Edit 2 snippet is the same data(message) that has been used for signing on PHP side.

kingSlayer
  • 1,149
  • 1
  • 10
  • 21
  • You should also try to decode the modulus using URLEncoding instead of StdEncoding like this: base64.URLEncoding.DecodeString(nStr) with my example – kingSlayer Sep 05 '16 at 00:08
  • Thank you so much for your interest! I have tried the above (and URLEncoding), it gives the same errors that I mention in the question ("crypto/rsa: input must be hashed message" and "crypto/rsa: verification error"). In your code, how do you call rsa.VerifyPKCS1v15? Do you combine authenticatorData and hash? – yngling Sep 06 '16 at 12:04
  • In the PHP sample code the verification is done by "return $rsa->verify($a . $h,$s);", but if I do the same in Go I get the error "crypto/rsa: input must be hashed message". – yngling Sep 06 '16 at 12:27
  • You can see my snippet in Edit 2 regarding the combining of hash and data. To verify the signature in PHP I use opensslverify(data, $signature,pubKey, OPENSSL_ALGO_SHA256) and the port to Go is working for me. Hope it helps. – kingSlayer Sep 06 '16 at 15:23
  • I really appreciate your help. It is difficult for me to know exactly what has been used to sign the message, as it happens in the webauthn part of Windows, but as C#, PHP and JavaScript sample code all combine the authenticatorData and hash arrays, I assume it is that. I get "crypto/rsa: verification error" if I use that though, so I am unsure what I am doing wrong. – yngling Sep 06 '16 at 22:07