Биометрический логин (webauthn) в Go, как проверить подпись
Благодаря новейшему обновлению Windows Anniversary, Edge теперь поддерживает биометрическую аутентификацию с использованием Windows Hello (см. 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/)
У меня есть несколько примеров в 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);
});
В go у меня есть следующий код, предназначенный для соответствия вышеуказанному 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 (см. 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 после создания открытого ключа, поэтому может возникнуть проблема с созданием открытого ключа.
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
}
Я сравнил свой открытый ключ и ваш ( игровая площадка), и они имеют разные значения. Не могли бы вы дать мне отзыв о предложенном мною решении с вашим кодом, если оно работает?
Edit 1: пример кодирования URLE Playground 2
Изменить 2: Вот как я проверяю подпись:
hasher := sha256.New()
hasher.Write([]byte(data))
err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hasher.Sum(nil), signature)
Таким образом, переменная data в фрагменте Edit 2 - это те же данные (сообщение), которые использовались для подписи на стороне PHP.