Биометрический вход (webauthn) в Go, как проверить подпись

В самом недавнем обновлении Windows Anniversary Edge теперь поддерживает биометрическую аутентификацию с помощью Windows Hello (см. https://developer.microsoft.com/en-us/microsoft-edge/platform/documentation/dev-guide/устройство/веб-аутентификация/, https://blogs.windows.com/msedgedev/2016/04/12/a-world-without-passwords-windows-hello-in-microsoft-edge/)

У меня есть несколько примеров на C #, PHP и Node.js, и я пытаюсь заставить их работать на Go.

Следующее работает в JS (я жестко запрограммировал задачу и ключ):

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);
});

У меня есть следующий код, который соответствует указанному выше JS-коду (req - это структура со строками из тела запроса JSON):

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

Этот код не работает с crypto/rsa: input must be hashed message. Если я перейду на hash[:] вместо b в rsa.VerifyPKCS1v15, это не сработает с crypto/rsa: verification error. Я считаю, что мне нужно комбинировать authenticatorData и hash, потому что это то, что происходит в примерах кодов C # и PHP (cf, https://github.com/adrianba/fido-snippets/blob/master/csharp/app.cs, https://github.com/adrianba/fido-snippets/blob/master/php/fido-Authenticator.php).

Может, в Go все по-другому?

Я распечатал байтовые массивы в JS и Go и убедился, что clientData, signatureData, authenticatorData и hash (и объединенный массив последних двух) имеют точно такие же значения. Мне не удалось извлечь поля n и e из JS после создания открытого ключа, поэтому может возникнуть проблема с тем, как я создаю открытый ключ.


person yngling    schedule 14.08.2016    source источник
comment
Если бы кто-то с 1500+ репутацией мог добавить тег под названием webauthn, это было бы здорово. (Это название стандарта, см. w3.org/Webauthn)   -  person yngling    schedule 14.08.2016


Ответы (1)


Я не специалист по криптографии, но у меня есть некоторый опыт работы с Go, включая проверку подписей, подписанных с помощью PHP. Итак, предполагая, что сравниваемые байтовые значения одинаковы, я бы сказал, что ваша проблема, вероятно, связана с созданием открытого ключа. Я бы предложил попробовать свое решение по созданию открытых ключей из модуля и экспоненты с помощью этой функции:

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
}

Я сравнил свой открытый ключ и Ваш (Playground), и у них разные значения. Не могли бы вы дать мне отзыв о решении, которое я предложил с вашим кодом, если оно работает?

Редактировать 1: пример URLEncoding Playground 2

Изменить 2. Я проверяю подпись следующим образом:

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

Таким образом, переменная data в фрагменте Edit 2 - это те же данные (сообщение), которые использовались для подписи на стороне PHP.

person kingSlayer    schedule 04.09.2016
comment
Вы также должны попытаться декодировать модуль, используя URLEncoding вместо StdEncoding, например: base64.URLEncoding.DecodeString (nStr) с моим примером - person kingSlayer; 05.09.2016
comment
Большое спасибо за проявленный интерес! Я пробовал вышеуказанное (и URLEncoding), он дает те же ошибки, которые я упоминаю в вопросе (crypto / rsa: ввод должен быть хешированным сообщением и crypto / rsa: ошибка проверки). Как в вашем коде вызвать rsa.VerifyPKCS1v15? Вы комбинируете AuthentatorData и хэш? - person yngling; 06.09.2016
comment
В примере кода PHP проверка выполняется с помощью return $ rsa- ›verify ($ a. $ H, $ s) ;, но если я сделаю то же самое в Go, я получаю сообщение об ошибке crypto / rsa: input must be hashed. - person yngling; 06.09.2016
comment
Вы можете увидеть мой фрагмент в Edit 2, касающийся объединения хэша и данных. Для проверки подписи в PHP я использую opensslverify (data, $ signature, pubKey, OPENSSL_ALGO_SHA256), и у меня работает порт Go. Надеюсь, это поможет. - person kingSlayer; 06.09.2016
comment
Я действительно ценю твою помощь. Мне сложно точно знать, что было использовано для подписи сообщения, как это происходит в части webauthn в Windows, но поскольку примеры кода C #, PHP и JavaScript объединяют массивы authenticationatorData и hash, я предполагаю, что это так. Я получаю ошибку crypto / rsa: verify, если использую это, поэтому я не уверен, что делаю неправильно. - person yngling; 07.09.2016