来自 Yubico 的 fido2.dll 的 Webauthn 凭证验证
Webauthn credential verifiation with fido2.dll fro Yubico
我开始在 Delphi 中连接 yubicos fido2.dll 并且能够连接它
根据提供的示例。现在我想更进一步,使用
例如上的 dll用于处理凭据创建和断言的 apache 服务器。
所以.. 为此,我基本上使用在测试站点 https://webauthn.io/
上找到的 javascript
基本上我想模仿一些服务器功能来创建凭据。在网站上可以设置
一些属性 - 在我的环境中,它们来自服务器。
目前我已经与客户端通信以发出凭据初始化 - 服务器响应挑战。查询密钥,浏览器创建凭据并将其发送回服务器。虽然我对来自服务器的数据有疑问,但我有一个
解码 attestationObject 部分时出现问题。
所以这是来自我的服务器的凭证初始化 json:
{"publicKey":{"challenge":"LFJYIdXJfYpB1GZS+PzEOD8DNcYmdia4mZp2z0J4QcE=","pubKeyCredParams":[{"alg":-7,"type":"public-key"},{"alg":-257,"type":"public-key"},{"alg":-8,"type":"public-key"}],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","userVerification":"required","requireResidentKey":false},"rp":{"id":"fidotest.com","name":"fidotest.com"},"user":{"id":"zVOUjBCxJNIbSSWNiGOv2\/kZP2UU8pPguVylFeiw4HE=","displayName":"test","name":"test"},"Timeout":60000,"attestation":"direct"}}
来自服务器的结果:
{"id":"MfcgyBxDxpq5S71fB45FFjecCGtvCepvb6IZexJpgaHyTPPsaz0srQyZc26HkE92eda7a2PmPIzvSpLbipktmw","rawId":"MfcgyBxDxpq5S71fB45FFjecCGtvCepvb6IZexJpgaHyTPPsaz0srQyZc26HkE92eda7a2PmPIzvSpLbipktmw","type":"public-key","response":{"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAO8fbE8iQcMFYE4KBwL6HK6OxSReRKriXZDWhcfGRMFxAiB7mIPZ7n-fWas7aWkEkWd-9CWvd8ncRVCh3BBFIzMuRmN4NWOBWQLAMIICvDCCAaSgAwIBAgIEBMX-_DANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgODAwODQ3MzIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQc2Np2EaP17x-IXpULpl2A4zSFU5FYS9R_W3GcUyNcJCHk45m9tXNngkGQk1dmYUk8kUwuZyTfk5T8-n3qixgEo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBD4oBHzjApNFYAGFxEfntx9MAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBAHcYTO91LRoF8wpThdwthvj6wGNxcLAiYqUZXPX-0Db-AGVODSkVvEVSmj-JXmrBzNQel3FW4AupOgbgrJmmcWWEBZyXSpRQtYcl2LTNU0-Iz9WbyHNN1wQJ9ybFwj608xBuoNRC0rG8wgYbMC4usyRadt3dYOVdQi0cfaksVB2VNKnw-ttQUWKoZsPHtuzFx8NlazLQBep1W2T0FCONFEG7x_l-ZcfNhT13azAbaurJ2J0_ff6H0PXJP6h-Obne4xfz0-8ujftWDUSh9oaiVRYf-tgam_tzOKyEU38V2liV11zMyHKWrXiK0AfyDgb58ky2HSrn_AgE5MW_oXg_CXdoYXV0aERhdGFYxNNxx2kdwL5GE4VmZm0_PerRaSEQdriBtCqmPBobyJXTRQAAAA34oBHzjApNFYAGFxEfntx9AEAx9yDIHEPGmrlLvV8HjkUWN5wIa28J6m9vohl7EmmBofJM8-xrPSytDJlzboeQT3Z51rtrY-Y8jO9KktuKmS2bpQECAyYgASFYIPVUDt7LCfuPyhdowBAhHCaRp-4acTmevkowvQhYxQluIlggCOL0rfuXgGze8yGX38sBXzsSMqxQxiskjsXia6UQvtQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJMRkpZSWRYSmZZcEIxR1pTLVB6RU9EOEROY1ltZGlhNG1acDJ6MEo0UWNFIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9maWRvdGVzdC5jb20iLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0"}}
这是一个应该验证凭据的控制台程序:
(注意你需要将上面的内容粘贴到一个文本文件中,然后在控制台中加载它)。
program VerifyCred;
{$APPTYPE CONSOLE}
uses
SysUtils,
Fido2 in '..\Fido2.pas',
Fido2dll in '..\Fido2dll.pas',
Fido2Json in '..\Fido2Json.pas',
windows,
classes,
cbor,
superobject;
function DoVerifyCred( credential : ISuperObject ) : string;
var clientData : ISuperObject;
s : string;
rawS : RawByteString;
credentialId : string;
rawId : TBytes;
credVerify : TFidoCredVerify;
cborItem : TCborMap;
sig : TBytes;
x5c : TBytes;
authData : TBytes;
fmt : string;
alg : integer;
i : integer;
aName : string;
res : boolean;
rawChallange : RawByteString;
credFMT : TFidoCredentialFmt;
challenge : TFidoChallenge;
authDataObj : TAuthData;
attStmt : TCborMap;
j : integer;
restBuf : TBytes;
begin
Result := '{"error":0,"msg":"Error parsing content"}';
s := credential.S['response.clientDataJSON'];
if s = '' then
exit;
ClientData := So( String(Base64URLDecode( s )) );
if clientData = nil then
exit;
rawChallange := Base64URLDecode(ClientData.S['challenge']);
if Length(rawChallange) <> sizeof(challenge) then
exit;
Move( rawChallange[1], challenge, sizeof(challenge));
clientData := SO( String( Base64URLDecode( credential.S['response.clientDataJSON'] ) ) );
s := credential.S['response.attestationObject'];
if s = '' then
exit;
// attestation object is a cbor encoded raw base64ulr encoded string
cborItem := TCborDecoding.DecodeBase64UrlEx(s, restBuf) as TCborMap;
if not Assigned(cborItem) then
exit;
try
alg := 0;
fmt := '';
sig := nil;
authData := nil;
x5c := nil;
for i := 0 to cborItem.Count - 1 do
begin
assert( cborItem.Names[i] is TCborUtf8String, 'CBOR type error');
aName := String((cborItem.Names[i] as TCborUtf8String).Value);
if SameText(aName, 'attStmt') then
begin
attStmt := cborItem.Values[i] as TCborMap;
for j := 0 to attStmt.Count - 1 do
begin
aName := String((attStmt.Names[j] as TCborUtf8String).Value);
if SameText(aName, 'alg')
then
alg := (attStmt.Values[j] as TCborNegIntItem).value
else if SameText(aName, 'sig')
then
sig := (attStmt.Values[j] as TCborByteString).ToBytes
else if SameText(aName, 'x5c')
then
x5c := ((attStmt.Values[j] as TCborArr)[0] as TCborByteString).ToBytes
end;
end
else if SameText(aName, 'authData')
then
authData := (cborItem.Values[i] as TCborByteString).ToBytes
else if SameText(aName, 'fmt')
then
fmt := String( (cborItem.Values[i] as TCborUtf8String).Value );
end;
finally
cborItem.Free;
end;
// check if anyhing is in place
if not (( alg = COSE_ES256 ) or (alg = COSE_EDDSA) or (alg= COSE_RS256)) then
raise Exception.Create('Unknown algorithm');
if Length(sig) = 0 then
raise Exception.Create('No sig field provided');
if Length(x5c) = 0 then
raise Exception.Create('No certificate');
if Length(authData) = 0 then
raise Exception.Create('Missing authdata');
credentialId := credential.S['id'];
s := credential.S['rawId'];
if s = '' then
raise Exception.Create('No Credential id found');
raws := Base64URLDecode( s );
SetLength( rawId, Length(rawS));
Move( rawS[1], rawId[0], Length(rawId));
authDataObj := nil;
if Length(restBuf) > 0 then
raise Exception.Create('Damend there is a rest buffer that should not be');
if Length(authDAta) > 0 then
authDataObj := TAuthData.Create( authData );
try
if fmt = 'packed'
then
credFmt := fmFido2
else if fmt = 'fido-u2f'
then
credFmt := fmU2F
else
credFmt := fmDef;
// now... verify the credentials
credVerify := TFidoCredVerify.Create( TFidoCredentialType(alg), credFmt,
authData, x5c, sig, FidoServer.RequireResidentKey,
authDataObj.UserVerified, 0) ;
try
// auth data seems bad!
res := credVerify.Verify(challenge);
if res then
begin
// -> save EVERYTHING to a database
end;
finally
credVerify.Free;
end;
finally
authDataObj.Free;
end;
// build result and generate a session
if res then
begin
// yeeeha we got a
Result := '{"success":true}';
end
else
Result := '{"success":false}';
end;
var credential: ISuperObject;
begin
try
with TStringList.Create do
try
LoadFromFile('D:\CredVerify_delphi.json');
credential := SO( Text );
finally
Free;
end;
Writeln( doVerifyCred(credential) );
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
cbor and fido2 个项目可以在 github 上找到。
我实际上对 CBOR 编码的 attestationObject returned 有疑问。如果设置常驻键 属性
证明对象只有 63 个字节长 - 剩下的字节实际上没有编码。所以...
那里的 cbor 解码要么失败,要么我取回的数据不符合应该位于的 webauthn 证明对象
这些位置 return 凭证 ID 和 public 密钥(然后也是 cbor 编码)。如果常驻键属性为false
正如上述语句中的情况,fido dll returns 错误的授权数据。所以...有人知道我做错了什么吗?
它基本上应该看起来像 the diagram 但它要么在凭证 ID 中间的 63 个字节之后结束,要么在 dll 中失败。
原来我误用了 indy base64 例程。 base64 解码器对于 Unicodstring 无法正常工作(我假设该字符串已转换为 ansistring ...)
所以我改为使用以下解码器:
function Base64Decode( s : string ) : RawByteString;
var aWrapStream : TWrapMemoryStream;
sconvStr : UTF8String;
lStream : TMemoryStream;
begin
sConvStr := UTF8String( s );
aWrapStream := TWrapMemoryStream.Create( @sConvStr[1], Length(sConvStr) );
lStream := TMemoryStream.Create;
try
with TIdDecoderMIME.Create(nil) do
try
DecodeBegin(lStream);
Decode( aWrapStream );
DecodeEnd;
SetLength(Result, lStream.Size );
if lStream.Size > 0 then
Move( PByte(lStream.Memory)^, Result[1], lStream.Size);
finally
Free;
end;
finally
lStream.Free;
end;
aWrapStream.Free;
end;
我开始在 Delphi 中连接 yubicos fido2.dll 并且能够连接它 根据提供的示例。现在我想更进一步,使用 例如上的 dll用于处理凭据创建和断言的 apache 服务器。
所以.. 为此,我基本上使用在测试站点 https://webauthn.io/
上找到的 javascript基本上我想模仿一些服务器功能来创建凭据。在网站上可以设置 一些属性 - 在我的环境中,它们来自服务器。
目前我已经与客户端通信以发出凭据初始化 - 服务器响应挑战。查询密钥,浏览器创建凭据并将其发送回服务器。虽然我对来自服务器的数据有疑问,但我有一个 解码 attestationObject 部分时出现问题。
所以这是来自我的服务器的凭证初始化 json:
{"publicKey":{"challenge":"LFJYIdXJfYpB1GZS+PzEOD8DNcYmdia4mZp2z0J4QcE=","pubKeyCredParams":[{"alg":-7,"type":"public-key"},{"alg":-257,"type":"public-key"},{"alg":-8,"type":"public-key"}],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","userVerification":"required","requireResidentKey":false},"rp":{"id":"fidotest.com","name":"fidotest.com"},"user":{"id":"zVOUjBCxJNIbSSWNiGOv2\/kZP2UU8pPguVylFeiw4HE=","displayName":"test","name":"test"},"Timeout":60000,"attestation":"direct"}}
来自服务器的结果:
{"id":"MfcgyBxDxpq5S71fB45FFjecCGtvCepvb6IZexJpgaHyTPPsaz0srQyZc26HkE92eda7a2PmPIzvSpLbipktmw","rawId":"MfcgyBxDxpq5S71fB45FFjecCGtvCepvb6IZexJpgaHyTPPsaz0srQyZc26HkE92eda7a2PmPIzvSpLbipktmw","type":"public-key","response":{"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAO8fbE8iQcMFYE4KBwL6HK6OxSReRKriXZDWhcfGRMFxAiB7mIPZ7n-fWas7aWkEkWd-9CWvd8ncRVCh3BBFIzMuRmN4NWOBWQLAMIICvDCCAaSgAwIBAgIEBMX-_DANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgODAwODQ3MzIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQc2Np2EaP17x-IXpULpl2A4zSFU5FYS9R_W3GcUyNcJCHk45m9tXNngkGQk1dmYUk8kUwuZyTfk5T8-n3qixgEo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBD4oBHzjApNFYAGFxEfntx9MAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBAHcYTO91LRoF8wpThdwthvj6wGNxcLAiYqUZXPX-0Db-AGVODSkVvEVSmj-JXmrBzNQel3FW4AupOgbgrJmmcWWEBZyXSpRQtYcl2LTNU0-Iz9WbyHNN1wQJ9ybFwj608xBuoNRC0rG8wgYbMC4usyRadt3dYOVdQi0cfaksVB2VNKnw-ttQUWKoZsPHtuzFx8NlazLQBep1W2T0FCONFEG7x_l-ZcfNhT13azAbaurJ2J0_ff6H0PXJP6h-Obne4xfz0-8ujftWDUSh9oaiVRYf-tgam_tzOKyEU38V2liV11zMyHKWrXiK0AfyDgb58ky2HSrn_AgE5MW_oXg_CXdoYXV0aERhdGFYxNNxx2kdwL5GE4VmZm0_PerRaSEQdriBtCqmPBobyJXTRQAAAA34oBHzjApNFYAGFxEfntx9AEAx9yDIHEPGmrlLvV8HjkUWN5wIa28J6m9vohl7EmmBofJM8-xrPSytDJlzboeQT3Z51rtrY-Y8jO9KktuKmS2bpQECAyYgASFYIPVUDt7LCfuPyhdowBAhHCaRp-4acTmevkowvQhYxQluIlggCOL0rfuXgGze8yGX38sBXzsSMqxQxiskjsXia6UQvtQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJMRkpZSWRYSmZZcEIxR1pTLVB6RU9EOEROY1ltZGlhNG1acDJ6MEo0UWNFIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9maWRvdGVzdC5jb20iLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0"}}
这是一个应该验证凭据的控制台程序: (注意你需要将上面的内容粘贴到一个文本文件中,然后在控制台中加载它)。
program VerifyCred;
{$APPTYPE CONSOLE}
uses
SysUtils,
Fido2 in '..\Fido2.pas',
Fido2dll in '..\Fido2dll.pas',
Fido2Json in '..\Fido2Json.pas',
windows,
classes,
cbor,
superobject;
function DoVerifyCred( credential : ISuperObject ) : string;
var clientData : ISuperObject;
s : string;
rawS : RawByteString;
credentialId : string;
rawId : TBytes;
credVerify : TFidoCredVerify;
cborItem : TCborMap;
sig : TBytes;
x5c : TBytes;
authData : TBytes;
fmt : string;
alg : integer;
i : integer;
aName : string;
res : boolean;
rawChallange : RawByteString;
credFMT : TFidoCredentialFmt;
challenge : TFidoChallenge;
authDataObj : TAuthData;
attStmt : TCborMap;
j : integer;
restBuf : TBytes;
begin
Result := '{"error":0,"msg":"Error parsing content"}';
s := credential.S['response.clientDataJSON'];
if s = '' then
exit;
ClientData := So( String(Base64URLDecode( s )) );
if clientData = nil then
exit;
rawChallange := Base64URLDecode(ClientData.S['challenge']);
if Length(rawChallange) <> sizeof(challenge) then
exit;
Move( rawChallange[1], challenge, sizeof(challenge));
clientData := SO( String( Base64URLDecode( credential.S['response.clientDataJSON'] ) ) );
s := credential.S['response.attestationObject'];
if s = '' then
exit;
// attestation object is a cbor encoded raw base64ulr encoded string
cborItem := TCborDecoding.DecodeBase64UrlEx(s, restBuf) as TCborMap;
if not Assigned(cborItem) then
exit;
try
alg := 0;
fmt := '';
sig := nil;
authData := nil;
x5c := nil;
for i := 0 to cborItem.Count - 1 do
begin
assert( cborItem.Names[i] is TCborUtf8String, 'CBOR type error');
aName := String((cborItem.Names[i] as TCborUtf8String).Value);
if SameText(aName, 'attStmt') then
begin
attStmt := cborItem.Values[i] as TCborMap;
for j := 0 to attStmt.Count - 1 do
begin
aName := String((attStmt.Names[j] as TCborUtf8String).Value);
if SameText(aName, 'alg')
then
alg := (attStmt.Values[j] as TCborNegIntItem).value
else if SameText(aName, 'sig')
then
sig := (attStmt.Values[j] as TCborByteString).ToBytes
else if SameText(aName, 'x5c')
then
x5c := ((attStmt.Values[j] as TCborArr)[0] as TCborByteString).ToBytes
end;
end
else if SameText(aName, 'authData')
then
authData := (cborItem.Values[i] as TCborByteString).ToBytes
else if SameText(aName, 'fmt')
then
fmt := String( (cborItem.Values[i] as TCborUtf8String).Value );
end;
finally
cborItem.Free;
end;
// check if anyhing is in place
if not (( alg = COSE_ES256 ) or (alg = COSE_EDDSA) or (alg= COSE_RS256)) then
raise Exception.Create('Unknown algorithm');
if Length(sig) = 0 then
raise Exception.Create('No sig field provided');
if Length(x5c) = 0 then
raise Exception.Create('No certificate');
if Length(authData) = 0 then
raise Exception.Create('Missing authdata');
credentialId := credential.S['id'];
s := credential.S['rawId'];
if s = '' then
raise Exception.Create('No Credential id found');
raws := Base64URLDecode( s );
SetLength( rawId, Length(rawS));
Move( rawS[1], rawId[0], Length(rawId));
authDataObj := nil;
if Length(restBuf) > 0 then
raise Exception.Create('Damend there is a rest buffer that should not be');
if Length(authDAta) > 0 then
authDataObj := TAuthData.Create( authData );
try
if fmt = 'packed'
then
credFmt := fmFido2
else if fmt = 'fido-u2f'
then
credFmt := fmU2F
else
credFmt := fmDef;
// now... verify the credentials
credVerify := TFidoCredVerify.Create( TFidoCredentialType(alg), credFmt,
authData, x5c, sig, FidoServer.RequireResidentKey,
authDataObj.UserVerified, 0) ;
try
// auth data seems bad!
res := credVerify.Verify(challenge);
if res then
begin
// -> save EVERYTHING to a database
end;
finally
credVerify.Free;
end;
finally
authDataObj.Free;
end;
// build result and generate a session
if res then
begin
// yeeeha we got a
Result := '{"success":true}';
end
else
Result := '{"success":false}';
end;
var credential: ISuperObject;
begin
try
with TStringList.Create do
try
LoadFromFile('D:\CredVerify_delphi.json');
credential := SO( Text );
finally
Free;
end;
Writeln( doVerifyCred(credential) );
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
cbor and fido2 个项目可以在 github 上找到。
我实际上对 CBOR 编码的 attestationObject returned 有疑问。如果设置常驻键 属性 证明对象只有 63 个字节长 - 剩下的字节实际上没有编码。所以... 那里的 cbor 解码要么失败,要么我取回的数据不符合应该位于的 webauthn 证明对象 这些位置 return 凭证 ID 和 public 密钥(然后也是 cbor 编码)。如果常驻键属性为false 正如上述语句中的情况,fido dll returns 错误的授权数据。所以...有人知道我做错了什么吗?
它基本上应该看起来像 the diagram 但它要么在凭证 ID 中间的 63 个字节之后结束,要么在 dll 中失败。
原来我误用了 indy base64 例程。 base64 解码器对于 Unicodstring 无法正常工作(我假设该字符串已转换为 ansistring ...) 所以我改为使用以下解码器:
function Base64Decode( s : string ) : RawByteString;
var aWrapStream : TWrapMemoryStream;
sconvStr : UTF8String;
lStream : TMemoryStream;
begin
sConvStr := UTF8String( s );
aWrapStream := TWrapMemoryStream.Create( @sConvStr[1], Length(sConvStr) );
lStream := TMemoryStream.Create;
try
with TIdDecoderMIME.Create(nil) do
try
DecodeBegin(lStream);
Decode( aWrapStream );
DecodeEnd;
SetLength(Result, lStream.Size );
if lStream.Size > 0 then
Move( PByte(lStream.Memory)^, Result[1], lStream.Size);
finally
Free;
end;
finally
lStream.Free;
end;
aWrapStream.Free;
end;