Skip to content

Commit

Permalink
feat(metadata): mds3 support (#54)
Browse files Browse the repository at this point in the history
This feature migrates from MDS2 to MDS3.
  • Loading branch information
aseigler committed Oct 8, 2022
1 parent fa14fa7 commit 697bc4c
Show file tree
Hide file tree
Showing 23 changed files with 1,107 additions and 557 deletions.
536 changes: 298 additions & 238 deletions metadata/metadata.go

Large diffs are not rendered by default.

347 changes: 297 additions & 50 deletions metadata/metadata_test.go

Large diffs are not rendered by default.

40 changes: 38 additions & 2 deletions protocol/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package protocol

import (
"crypto/sha256"
"crypto/x509"
"encoding/json"
"fmt"

"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/google/uuid"
)

// From §5.2.1 (https://www.w3.org/TR/webauthn/#authenticatorattestationresponse)
Expand Down Expand Up @@ -52,7 +55,6 @@ type ParsedAttestationResponse struct {
// perform self attestation of the credential public key with the corresponding credential private key.
// All this information is returned by authenticators any time a new public key credential is generated, in
// the overall form of an attestation object. (https://www.w3.org/TR/webauthn/#attestation-object)
//
type AttestationObject struct {
// The authenticator data, including the newly created public key. See AuthenticatorData for more info
AuthData AuthenticatorData
Expand Down Expand Up @@ -147,10 +149,44 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client
// Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using
// the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized
// client data computed in step 7.
attestationType, _, err := formatHandler(*attestationObject, clientDataHash)
attestationType, x5c, err := formatHandler(*attestationObject, clientDataHash)
if err != nil {
return err.(*Error).WithInfo(attestationType)
}

uuid, err := uuid.FromBytes(attestationObject.AuthData.AttData.AAGUID)
if err != nil {
return err
}
if meta, ok := metadata.Metadata[uuid]; ok {
for _, s := range meta.StatusReports {
if metadata.IsUndesiredAuthenticatorStatus(s.Status) {
return ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered")
}
}

if x5c != nil {
attestnCert, err := x509.ParseCertificate(x5c[0].([]byte))
if err != nil {
return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c")
}
if attestnCert.Subject.CommonName != attestnCert.Issuer.CommonName {
var hasBasicFull = false
for _, a := range meta.MetadataStatement.AttestationTypes {
if metadata.AuthenticatorAttestationType(a) == metadata.BasicFull {
hasBasicFull = true
}
}
if !hasBasicFull {
return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authentictor that does not support full attestation")
}
}
}
} else {
if metadata.Conformance {
return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during conformance testing", uuid.String()))
}
}

return nil
}
37 changes: 19 additions & 18 deletions protocol/attestation_androidkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/asn1"
"fmt"

"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)

Expand Down Expand Up @@ -38,51 +39,51 @@ func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (strin
// used to generate the attestation signature.
alg, present := att.AttStatement["alg"].(int64)
if !present {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
return "", nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
}

// Get the sig value - A byte string containing the attestation signature.
sig, present := att.AttStatement["sig"].([]byte)
if !present {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
return "", nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
}

// If x5c is not present, return an error
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value")
return "", nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value")
}

// §8.4.2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
// using the public key in the first certificate in x5c with the algorithm specified in alg.
attCertBytes, valid := x5c[0].([]byte)
if !valid {
return androidAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
return "", nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}

signatureData := append(att.RawAuthData, clientDataHash...)

attCert, err := x509.ParseCertificate(attCertBytes)
if err != nil {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}

coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)

if err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, sig); err != nil {
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v", err))
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, sig)
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
}
// Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
if err != nil {
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v", err))
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
}
e := pubKey.(webauthncose.EC2PublicKeyData)
valid, err = e.Verify(signatureData, sig)
if err != nil || !valid {
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v", err))
if err != nil || valid != true {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
}
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
// attCert.Extensions
Expand All @@ -93,33 +94,33 @@ func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (strin
}
}
if len(attExtBytes) == 0 {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
}
// As noted in §8.4.1 (https://w3c.github.io/webauthn/#key-attstn-cert-requirements) the Android Key Attestation attestation certificate's
// android key attestation certificate extension data is identified by the OID "1.3.6.1.4.1.11129.2.1.17".
decoded := keyDescription{}
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded)
if err != nil {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions")
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions")
}
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
if 0 != bytes.Compare(decoded.AttestationChallenge, clientDataHash) {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash")
return "", nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash")
}
// The AuthorizationList.allApplications field is not present on either authorization list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
if nil != decoded.SoftwareEnforced.AllApplications || nil != decoded.TeeEnforced.AllApplications {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field")
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field")
}
// For the following, use only the teeEnforced authorization list if the RP wants to accept only keys from a trusted execution environment, otherwise use the union of teeEnforced and softwareEnforced.
// The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED. (which == 0)
if KM_ORIGIN_GENERATED != decoded.SoftwareEnforced.Origin || KM_ORIGIN_GENERATED != decoded.TeeEnforced.Origin {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED")
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED")
}
// The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN. (which == 2)
if !contains(decoded.SoftwareEnforced.Purpose, KM_PURPOSE_SIGN) && !contains(decoded.TeeEnforced.Purpose, KM_PURPOSE_SIGN) {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN")
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN")
}
return androidAttestationKey, x5c, err
return string(metadata.BasicFull), x5c, err
}

func contains(s []int, e int) bool {
Expand Down
86 changes: 86 additions & 0 deletions protocol/attestation_androidkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package protocol

import (
"crypto/sha256"
"testing"

"github.com/go-webauthn/webauthn/metadata"
)

func TestVerifyAndroidKeyFormat(t *testing.T) {
type args struct {
att AttestationObject
clientDataHash []byte
}
successAttResponse0 := attestationTestUnpackResponse(t, androidKeyTestResponse0["success"]).Response.AttestationObject
successClientDataHash0 := sha256.Sum256(attestationTestUnpackResponse(t, androidKeyTestResponse0["success"]).Raw.AttestationResponse.ClientDataJSON)
successAttResponse1 := attestationTestUnpackResponse(t, androidKeyTestResponse1["success"]).Response.AttestationObject
successClientDataHash1 := sha256.Sum256(attestationTestUnpackResponse(t, androidKeyTestResponse1["success"]).Raw.AttestationResponse.ClientDataJSON)
tests := []struct {
name string
args args
want string
want1 []interface{}
wantErr bool
}{
{
"success",
args{
successAttResponse0,
successClientDataHash0[:],
},
string(metadata.BasicFull),
nil,
false,
},
{
"success",
args{
successAttResponse1,
successClientDataHash1[:],
},
string(metadata.BasicFull),
nil,
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, _, err := verifyAndroidKeyFormat(tt.args.att, tt.args.clientDataHash)
if (err != nil) != tt.wantErr {
t.Errorf("verifyAndroidKeyFormat() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("verifyAndroidKeyFormat() got = %v, want %v", got, tt.want)
}
//if !reflect.DeepEqual(got1, tt.want1) {
// t.Errorf("verifySafetyNetFormat() got1 = %v, want %v", got1, tt.want1)
//}
})
}
}

var androidKeyTestResponse0 = map[string]string{
`success`: `{
"rawId": "U5cxFNxLbU9-SAi1K7k9atYwXhghkAMbxpL__VPtBlw",
"id": "U5cxFNxLbU9-SAi1K7k9atYwXhghkAMbxpL__VPtBlw",
"response": {
"clientDataJSON": "eyJvcmlnaW4iOiJodHRwczovL2xvY2FsaG9zdDo0NDMyOSIsImNoYWxsZW5nZSI6IjlNNWY3bGp5MVl2UWNzOE9pV1FWQ3ciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
"attestationObject": "o2NmbXRrYW5kcm9pZC1rZXlnYXR0U3RtdKNjYWxnJmNzaWdYSDBGAiEAlbQ-jtl8o9GtEstcEFH1Z_NlYsTYSn96lilEF17oEsMCIQDza5_axjn2jKZO63RlVf47DDFZbceW9b_tsh1nwOYQbmN4NWOCWQMFMIIDATCCAqegAwIBAgIBATAKBggqhkjOPQQDAjCBzjFFMEMGA1UEAww8RkFLRSBBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIEludGVybWVkaWF0ZSBGQUtFMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMCAXDTcwMDIwMTAwMDAwMFoYDzIwOTkwMTMxMjM1OTU5WjApMScwJQYDVQQDDB5GQUtFIEFuZHJvaWQgS2V5c3RvcmUgS2V5IEZBS0UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQbh-BQBJz7JeQ27dVvu3tyRieiEeXyDYoaWatRdy_D7q3TK96jumKlwIl5ZA2zHmKNLz4K2zsANq1X4tHp8MNZo4IBFjCCARIwCwYDVR0PBAQDAgeAMIHhBgorBgEEAdZ5AgERBIHSMIHPAgECCgEAAgEBCgEABCDc0UoXtU1CwwItW3ne2faKDcFCabFI31BufXEFVK_ENwQAMGm_hT0IAgYBXtPjz6C_hUVZBFcwVTEvMC0EKGNvbS5hbmRyb2lkLmtleXN0b3JlLmFuZHJvaWRrZXlzdG9yZWRlbW8CAQExIgQgdM_LUHSI9SkQhZHHpQWRnzJ3MvvB2ANSauqYAAbS2JgwMqEFMQMCAQKiAwIBA6MEAgIBAKUFMQMCAQSqAwIBAb-DeAMCAQK_hT4DAgEAv4U_AgUAMB8GA1UdIwQYMBaAFFKaGzLgVqrNUQ_vX4A3BovykSMdMAoGCCqGSM49BAMCA0gAMEUCIQDAPV7eQIWfL5BCmj82NszDlQ2IJsOZq_WxidwxD7On_QIgFipplgUF6OHvmHiDdaHJfFweeo60OtCDGDftjQEmF7FZAu4wggLqMIICkaADAgECAgECMAoGCCqGSM49BAMCMIHGMT0wOwYDVQQDDDRGQUtFIEFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdCBGQUtFMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDUwOTEyMzE0NFoXDTQ1MDkyNDEyMzE0NFowgc4xRTBDBgNVBAMMPEZBS0UgQW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBJbnRlcm1lZGlhdGUgRkFLRTExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKtQYStiTRe7w7UbBEk7BUkLjB-LnbzzebLe3KB8UqHXtg3TIXXcK37dvCbbCNVfhvZxtpTcME2kooqMTgOm9cejZjBkMBIGA1UdEwEB_wQIMAYBAf8CAQAwDgYDVR0PAQH_BAQDAgKEMB0GA1UdDgQWBBSj0qos7w2M8iQC1Ry0YLy_alskFDAfBgNVHSMEGDAWgBRSmhsy4FaqzVEP71-ANwaL8pEjHTAKBggqhkjOPQQDAgNHADBEAiBp3Z6j8YH7Qko5rRoK37nS4zPXhv65RWBV-j3MmXi50gIgPtMPpvcGtVbpFCQqsGbyhxPdkji8ltcYXQVfMhdUpRZoYXV0aERhdGFYpEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAFpVDktUqkdAn5qVGrdsEwExACBTlzEU3EttT35ICLUruT1q1jBeGCGQAxvGkv_9U-0GXKUBAgMmIAEhWCAbh-BQBJz7JeQ27dVvu3tyRieiEeXyDYoaWatRdy_D7iJYIK3TK96jumKlwIl5ZA2zHmKNLz4K2zsANq1X4tHp8MNZ"
},
"type": "public-key"
}`,
}

var androidKeyTestResponse1 = map[string]string{
`success`: `{
"id": "V51GE29tGbhby7sbg1cZ_qL8V8njqEsXpAnwQBobvgw",
"rawId": "V51GE29tGbhby7sbg1cZ_qL8V8njqEsXpAnwQBobvgw",
"response": {
"attestationObject": "o2NmbXRrYW5kcm9pZC1rZXlnYXR0U3RtdKNjYWxnJmNzaWdYRzBFAiAbZhfcF0KSXj5rdEevvnBcC8ZfRQlNl9XYWRTiIGKSHwIhAIerc7jWjOF_lJ71n_GAcaHwDUtPxkjAAdYugnZ4QxkmY3g1Y4JZAxowggMWMIICvaADAgECAgEBMAoGCCqGSM49BAMCMIHkMUUwQwYDVQQDDDxGQUtFIEFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gSW50ZXJtZWRpYXRlIEZBS0UxMTAvBgkqhkiG9w0BCQEWImNvbmZvcm1hbmNlLXRvb2xzQGZpZG9hbGxpYW5jZS5vcmcxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMCAXDTcwMDIwMTAwMDAwMFoYDzIwOTkwMTMxMjM1OTU5WjApMScwJQYDVQQDDB5GQUtFIEFuZHJvaWQgS2V5c3RvcmUgS2V5IEZBS0UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARuowgSu5AoRj8Vi_ZNSFBbGUZJXFG9MkDT6jADlr7tOK9NEgjVX53-ergXpyPaFZrAR9py-xnzfjILn_Kzb8Iqo4IBFjCCARIwCwYDVR0PBAQDAgeAMIHhBgorBgEEAdZ5AgERBIHSMIHPAgECCgEAAgEBCgEABCCfVEl83pSDSerk9I3pcICNTdzc5N3u4jt21cXdzBuJjgQAMGm_hT0IAgYBXtPjz6C_hUVZBFcwVTEvMC0EKGNvbS5hbmRyb2lkLmtleXN0b3JlLmFuZHJvaWRrZXlzdG9yZWRlbW8CAQExIgQgdM_LUHSI9SkQhZHHpQWRnzJ3MvvB2ANSauqYAAbS2JgwMqEFMQMCAQKiAwIBA6MEAgIBAKUFMQMCAQSqAwIBAb-DeAMCAQK_hT4DAgEAv4U_AgUAMB8GA1UdIwQYMBaAFKPSqizvDYzyJALVHLRgvL9qWyQUMAoGCCqGSM49BAMCA0cAMEQCIC7WHb2PyULnjp1M1TVI3Wti_eDhe6sFweuQAdecXtHhAiAS_eZkFsx_VNsrTu3XfZ2D7wIt-vT6nTljfHZ4zqU5xlkDGDCCAxQwggK6oAMCAQICAQIwCgYIKoZIzj0EAwIwgdwxPTA7BgNVBAMMNEZBS0UgQW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290IEZBS0UxMTAvBgkqhkiG9w0BCQEWImNvbmZvcm1hbmNlLXRvb2xzQGZpZG9hbGxpYW5jZS5vcmcxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE5MDQyNTA1NDkzMloXDTQ2MDkxMDA1NDkzMlowgeQxRTBDBgNVBAMMPEZBS0UgQW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBJbnRlcm1lZGlhdGUgRkFLRTExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASrUGErYk0Xu8O1GwRJOwVJC4wfi52883my3tygfFKh17YN0yF13Ct-3bwm2wjVX4b2cbaU3DBNpKKKjE4DpvXHo2MwYTAPBgNVHRMBAf8EBTADAQH_MA4GA1UdDwEB_wQEAwIChDAdBgNVHQ4EFgQUo9KqLO8NjPIkAtUctGC8v2pbJBQwHwYDVR0jBBgwFoAUUpobMuBWqs1RD-9fgDcGi_KRIx0wCgYIKoZIzj0EAwIDSAAwRQIhALFvLkAvtHrObTmN8P0-yLIT496P_weSEEbB6vCJWSh9AiBu-UOorCeLcF4WixOG9E5Li2nXe4uM2q6mbKGkll8u-WhhdXRoRGF0YVikPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBBAAAAYFUOS1SqR0CfmpUat2wTATEAIFedRhNvbRm4W8u7G4NXGf6i_FfJ46hLF6QJ8EAaG74MpQECAyYgASFYIG6jCBK7kChGPxWL9k1IUFsZRklcUb0yQNPqMAOWvu04Ilggr00SCNVfnf56uBenI9oVmsBH2nL7GfN-Mguf8rNvwio",
"clientDataJSON": "eyJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJjaGFsbGVuZ2UiOiI0YWI3ZGZkMS1hNjk1LTQ3NzctOTg1Zi1hZDI5OTM4MjhlOTkiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0"
},
"type": "public-key"
}`,
}
Loading

0 comments on commit 697bc4c

Please sign in to comment.