Goautom biometric login (webauthn), how to verify signature

With a recent Windows update, Anniversary 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 I'm trying to get it to work in Go.

The following works in JS (I am hard-coded in a call and 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 designed to match the above JS code (req is a structure with lines from the body of the JSON request):

 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(kN) if err != nil {   return err } e, err := webauthnSigninConversion(kE) 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 \ ": \" RSA \ ", \" alg \ ": \" RS256 \ ", \" ext \ ": false, \" n \ ": \" mEqGJwp0GL1oVwjRikkNfzd-Rkpb7vIbGodwQkTDsZT4_UE02WDaRa-PjxzL4lPZ4rUpV5SqVxM25aEIeGkEOR_8Xoqx7lpNKNOQs3E_o8hGBzQKpGcA7de678LeAUZdJZcnnQxXYjNf8St3aOIay7QrPoK8wQHEvv8Jqg7O1 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(kN) if err != nil {   return err } e, err := webauthnSigninConversion(kE) 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 ", \" e \ ": \" AQAB \ "}" // this is really from a db, not hardcoded 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(kN) if err != nil {   return err } e, err := webauthnSigninConversion(kE) 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 does not work with crypto/rsa: input must be hashed message . If I switch to using hash[:] instead of b in rsa.VerifyPKCS1v15 , it is not with crypto/rsa: verification error . The reason why it seems to me that I need to combine authenticatorData and hash is because this is what happens in the C # and PHP example code (cf, https://github.com/adrianba/fido-snippets/blob/master/ csharp / app.cs , https://github.com/adrianba/fido-snippets/blob/master/php/fido-authenticator.php ).

Is this possible in a different way?

I printed byte arrays in JS and Go and confirmed that clientData , signatureData , authenticatorData and hash (and the combined array of the last two) have the same values. I could not extract the n and e fields from JS after creating the public key, so there may be a problem with the way I create the public key.

+8
cryptography go rsa windows-hello webauthn
source share
1 answer

I'm not a cryptography specialist, but I have some experience with Go, including checking signatures that were signed with PHP. Thus, assuming that the byte values ​​being compared are the same, I would say that your problem is probably creating a public key. I would suggest trying my solution to creating public keys for a module and an exponent using 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 give me feedback on the solution that I proposed with your code, if it works?

Edit 1 : URLEncoding Playground Example 2

Change 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) 

Thus, the variable 'data' in the Edit 2 fragment is the same data (message) that was used for signing on the PHP side.

+2
source share

All Articles