Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Biometric login (webauthn) in Go, how to verify signature

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.cs , https://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.

like image 887
yngling Avatar asked Aug 14 '16 08:08

yngling


People also ask

What is signing in with biometrics?

Biometric login provides a convenient method for authorizing access to private content within your app. Instead of having to remember an account username and password every time they open your app, users can just use their biometric credentials to confirm their presence and authorize access to the private content.

Does Google support WebAuthn?

Browser supportWebAuthn is supported in Chrome, Firefox, and Edge, and Safari. It's written by the W3C and FIDO, with the participation of Google, Mozilla, Microsoft, Yubico, and others.

How do I use Web authentication API?

Server - the Web Authentication API is intended to register new credentials on a server (also referred to as a service or a relying party) and later use those same credentials on that same server to authenticate a user. Authenticator - the credentials are created and stored in a device called an authenticator.


1 Answers

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.

like image 135
kingSlayer Avatar answered Oct 01 '22 17:10

kingSlayer