mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
445 lines
14 KiB
Go
445 lines
14 KiB
Go
package keeper_test
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/sonr-io/sonr/x/did/keeper"
|
|
)
|
|
|
|
// WebAuthnIntegrationTestSuite tests end-to-end WebAuthn flows
|
|
type WebAuthnIntegrationTestSuite struct {
|
|
suite.Suite
|
|
f *testFixture
|
|
}
|
|
|
|
func TestWebAuthnIntegrationSuite(t *testing.T) {
|
|
suite.Run(t, new(WebAuthnIntegrationTestSuite))
|
|
}
|
|
|
|
func (suite *WebAuthnIntegrationTestSuite) SetupTest() {
|
|
suite.f = SetupTest(suite.T())
|
|
}
|
|
|
|
// TestCompleteRegistrationFlow tests the full WebAuthn registration process
|
|
func (suite *WebAuthnIntegrationTestSuite) TestCompleteRegistrationFlow() {
|
|
// Test data
|
|
username := "alice"
|
|
credentialID := "test-credential-123"
|
|
|
|
// Create valid attestation object (simplified for testing)
|
|
attestationObj := createTestAttestationObject(credentialID)
|
|
clientDataJSON := createTestClientDataJSON("test-challenge", "http://localhost:8080")
|
|
|
|
// Extract public key for registration (normally done by VerifyWebAuthnRegistration)
|
|
coseKey := map[int]any{
|
|
1: 2, // kty: EC2
|
|
3: -7, // alg: ES256
|
|
-1: 1, // crv: P-256
|
|
-2: make([]byte, 32), // x coordinate
|
|
-3: make([]byte, 32), // y coordinate
|
|
}
|
|
publicKeyCOSE, _ := cbor.Marshal(coseKey)
|
|
|
|
regData := &keeper.WebAuthnRegistrationData{
|
|
CredentialID: base64.RawURLEncoding.EncodeToString([]byte(credentialID)),
|
|
RawID: base64.RawURLEncoding.EncodeToString([]byte(credentialID)),
|
|
ClientDataJSON: base64.RawURLEncoding.EncodeToString(clientDataJSON),
|
|
AttestationObject: base64.RawURLEncoding.EncodeToString(attestationObj),
|
|
Username: username,
|
|
PublicKey: publicKeyCOSE,
|
|
Algorithm: -7, // ES256
|
|
}
|
|
|
|
// Process registration
|
|
didDoc, err := suite.f.k.ProcessWebAuthnRegistration(suite.f.ctx, regData)
|
|
suite.Require().NoError(err, "registration should succeed")
|
|
suite.Require().NotNil(didDoc)
|
|
|
|
// Verify DID document was created
|
|
suite.Require().Contains(didDoc.Id, "did:sonr:")
|
|
suite.Require().Len(didDoc.VerificationMethod, 1)
|
|
|
|
// Verify WebAuthn credential was stored
|
|
vm := didDoc.VerificationMethod[0]
|
|
suite.Require().NotNil(vm.WebauthnCredential)
|
|
suite.Require().
|
|
Equal(base64.RawURLEncoding.EncodeToString([]byte(credentialID)), vm.WebauthnCredential.CredentialId)
|
|
}
|
|
|
|
// TestCredentialIDUniqueness tests that duplicate credential IDs are rejected
|
|
func (suite *WebAuthnIntegrationTestSuite) TestCredentialIDUniqueness() {
|
|
credentialID := "unique-credential-456"
|
|
|
|
// First registration
|
|
regData1 := createTestRegistrationData("user1", credentialID)
|
|
didDoc1, err := suite.f.k.ProcessWebAuthnRegistration(suite.f.ctx, regData1)
|
|
suite.Require().NoError(err, "first registration should succeed")
|
|
suite.Require().NotNil(didDoc1)
|
|
|
|
// Attempt duplicate registration
|
|
regData2 := createTestRegistrationData("user2", credentialID)
|
|
_, err = suite.f.k.ProcessWebAuthnRegistration(suite.f.ctx, regData2)
|
|
suite.Require().Error(err, "duplicate credential ID should be rejected")
|
|
suite.Require().Contains(err.Error(), "already exists")
|
|
}
|
|
|
|
// TestMultiAlgorithmSupport tests different signature algorithms
|
|
func (suite *WebAuthnIntegrationTestSuite) TestMultiAlgorithmSupport() {
|
|
testCases := []struct {
|
|
name string
|
|
algorithm int32
|
|
keySize int
|
|
}{
|
|
{"ES256", -7, 64}, // ECDSA P-256
|
|
{"RS256", -257, 256}, // RSA
|
|
// Note: EdDSA (-8) is not currently supported by ValidateAlgorithmSupport
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
credentialID := fmt.Sprintf("algo-test-%s", tc.name)
|
|
username := fmt.Sprintf("user-%s", tc.name)
|
|
regData := createTestRegistrationDataWithAlgorithm(username, credentialID, tc.algorithm)
|
|
|
|
didDoc, err := suite.f.k.ProcessWebAuthnRegistration(suite.f.ctx, regData)
|
|
suite.Require().NoError(err, "registration with %s should succeed", tc.name)
|
|
suite.Require().NotNil(didDoc)
|
|
|
|
vm := didDoc.VerificationMethod[0]
|
|
suite.Require().Equal(tc.algorithm, vm.WebauthnCredential.Algorithm)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOriginValidation tests that only allowed origins are accepted
|
|
func (suite *WebAuthnIntegrationTestSuite) TestOriginValidation() {
|
|
testCases := []struct {
|
|
name string
|
|
origin string
|
|
shouldError bool
|
|
}{
|
|
{"valid localhost", "http://localhost:8080", false},
|
|
{"valid localhost alt port", "http://localhost:8081", false},
|
|
{"invalid origin", "http://evil.com", true},
|
|
{"invalid protocol", "ftp://localhost:8080", true},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
challenge := "test-challenge"
|
|
credentialID := fmt.Sprintf("origin-test-%s", tc.name)
|
|
|
|
clientData := createTestClientDataJSON(challenge, tc.origin)
|
|
attestationObj := createTestAttestationObject(credentialID)
|
|
|
|
// Create a valid COSE public key for ES256
|
|
coseKey := map[int]any{
|
|
1: 2, // kty: EC2
|
|
3: -7, // alg: ES256
|
|
-1: 1, // crv: P-256
|
|
-2: make([]byte, 32), // x coordinate
|
|
-3: make([]byte, 32), // y coordinate
|
|
}
|
|
publicKey, _ := cbor.Marshal(coseKey)
|
|
|
|
regData := &keeper.WebAuthnRegistrationData{
|
|
CredentialID: base64.RawURLEncoding.EncodeToString([]byte(credentialID)),
|
|
RawID: base64.RawURLEncoding.EncodeToString([]byte(credentialID)),
|
|
ClientDataJSON: base64.RawURLEncoding.EncodeToString(clientData),
|
|
AttestationObject: base64.RawURLEncoding.EncodeToString(attestationObj),
|
|
Username: "testuser",
|
|
PublicKey: publicKey,
|
|
Algorithm: -7, // ES256
|
|
Origin: tc.origin,
|
|
}
|
|
|
|
err := suite.f.k.VerifyWebAuthnRegistration(suite.f.ctx, regData, challenge)
|
|
|
|
if tc.shouldError {
|
|
suite.Require().Error(err, "origin %s should be rejected", tc.origin)
|
|
} else {
|
|
suite.Require().NoError(err, "origin %s should be accepted", tc.origin)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestChallengeVerification tests challenge validation
|
|
func (suite *WebAuthnIntegrationTestSuite) TestChallengeVerification() {
|
|
credentialID := "challenge-test-789"
|
|
correctChallenge := "correct-challenge"
|
|
wrongChallenge := "wrong-challenge"
|
|
|
|
// Create registration data with correct challenge
|
|
clientData := createTestClientDataJSON(correctChallenge, "http://localhost:8080")
|
|
attestationObj := createTestAttestationObject(credentialID)
|
|
|
|
// Create a valid COSE public key for ES256
|
|
coseKey := map[int]any{
|
|
1: 2, // kty: EC2
|
|
3: -7, // alg: ES256
|
|
-1: 1, // crv: P-256
|
|
-2: make([]byte, 32), // x coordinate
|
|
-3: make([]byte, 32), // y coordinate
|
|
}
|
|
publicKey, _ := cbor.Marshal(coseKey)
|
|
|
|
regData := &keeper.WebAuthnRegistrationData{
|
|
CredentialID: base64.RawURLEncoding.EncodeToString([]byte(credentialID)),
|
|
RawID: base64.RawURLEncoding.EncodeToString([]byte(credentialID)),
|
|
ClientDataJSON: base64.RawURLEncoding.EncodeToString(clientData),
|
|
AttestationObject: base64.RawURLEncoding.EncodeToString(attestationObj),
|
|
Username: "testuser",
|
|
PublicKey: publicKey,
|
|
Algorithm: -7, // ES256
|
|
Origin: "http://localhost:8080",
|
|
}
|
|
|
|
// Verify with correct challenge
|
|
err := suite.f.k.VerifyWebAuthnRegistration(suite.f.ctx, regData, correctChallenge)
|
|
suite.Require().NoError(err, "correct challenge should pass")
|
|
|
|
// Verify with wrong challenge
|
|
err = suite.f.k.VerifyWebAuthnRegistration(suite.f.ctx, regData, wrongChallenge)
|
|
suite.Require().Error(err, "wrong challenge should fail")
|
|
suite.Require().Contains(err.Error(), "challenge mismatch")
|
|
}
|
|
|
|
// TestDIDDocumentStorage tests that DID documents are properly stored
|
|
func (suite *WebAuthnIntegrationTestSuite) TestDIDDocumentStorage() {
|
|
username := "bob"
|
|
credentialID := "storage-test-abc"
|
|
|
|
regData := createTestRegistrationData(username, credentialID)
|
|
didDoc, err := suite.f.k.ProcessWebAuthnRegistration(suite.f.ctx, regData)
|
|
suite.Require().NoError(err)
|
|
suite.Require().NotNil(didDoc)
|
|
|
|
// Verify we can retrieve the stored DID document
|
|
credentials, err := suite.f.k.GetWebAuthnCredentialsByDID(suite.f.ctx, didDoc.Id)
|
|
suite.Require().NoError(err)
|
|
suite.Require().Len(credentials, 1)
|
|
suite.Require().
|
|
Equal(base64.RawURLEncoding.EncodeToString([]byte(credentialID)), credentials[0].CredentialId)
|
|
}
|
|
|
|
// TestInvalidAttestationHandling tests rejection of invalid attestation data
|
|
func (suite *WebAuthnIntegrationTestSuite) TestInvalidAttestationHandling() {
|
|
testCases := []struct {
|
|
name string
|
|
attestationObject string
|
|
clientDataJSON string
|
|
expectedError string
|
|
}{
|
|
{
|
|
"empty attestation",
|
|
"",
|
|
base64.RawURLEncoding.EncodeToString(
|
|
[]byte(
|
|
`{"type":"webauthn.create","challenge":"test","origin":"http://localhost:8080"}`,
|
|
),
|
|
),
|
|
"attestation_object is required",
|
|
},
|
|
{
|
|
"invalid base64",
|
|
"not-base64!@#$",
|
|
base64.RawURLEncoding.EncodeToString(
|
|
[]byte(
|
|
`{"type":"webauthn.create","challenge":"test","origin":"http://localhost:8080"}`,
|
|
),
|
|
),
|
|
"illegal base64 data",
|
|
},
|
|
{
|
|
"empty client data",
|
|
base64.RawURLEncoding.EncodeToString(createTestAttestationObject("test")),
|
|
"",
|
|
"failed to parse client data: unexpected end of JSON input",
|
|
},
|
|
{
|
|
"invalid client data JSON",
|
|
base64.RawURLEncoding.EncodeToString(createTestAttestationObject("test")),
|
|
base64.RawURLEncoding.EncodeToString([]byte("not json")),
|
|
"failed to decode client data JSON: illegal base64 data",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
// Create a valid public key for the test
|
|
coseKey := map[int]any{
|
|
1: 2, // kty: EC2
|
|
3: -7, // alg: ES256
|
|
-1: 1, // crv: P-256
|
|
-2: make([]byte, 32), // x coordinate
|
|
-3: make([]byte, 32), // y coordinate
|
|
}
|
|
publicKey, _ := cbor.Marshal(coseKey)
|
|
|
|
regData := &keeper.WebAuthnRegistrationData{
|
|
CredentialID: "test",
|
|
RawID: base64.RawURLEncoding.EncodeToString([]byte("test")),
|
|
ClientDataJSON: tc.clientDataJSON,
|
|
AttestationObject: tc.attestationObject,
|
|
Username: "testuser",
|
|
PublicKey: publicKey,
|
|
Algorithm: -7,
|
|
Origin: "http://localhost:8080",
|
|
}
|
|
|
|
err := suite.f.k.VerifyWebAuthnRegistration(suite.f.ctx, regData, "test")
|
|
suite.Require().Error(err)
|
|
suite.Require().Contains(err.Error(), tc.expectedError)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func createTestRegistrationData(username, credentialID string) *keeper.WebAuthnRegistrationData {
|
|
return createTestRegistrationDataWithAlgorithm(username, credentialID, -7) // ES256
|
|
}
|
|
|
|
func createTestRegistrationDataWithAlgorithm(
|
|
username, credentialID string,
|
|
algorithm int32,
|
|
) *keeper.WebAuthnRegistrationData {
|
|
attestationObj := createTestAttestationObject(credentialID)
|
|
clientDataJSON := createTestClientDataJSON("test-challenge", "http://localhost:8080")
|
|
|
|
// Create COSE public key based on algorithm
|
|
var publicKey []byte
|
|
switch algorithm {
|
|
case -7: // ES256
|
|
coseKey := map[int]any{
|
|
1: 2, // kty: EC2
|
|
3: -7, // alg: ES256
|
|
-1: 1, // crv: P-256
|
|
-2: make([]byte, 32), // x coordinate
|
|
-3: make([]byte, 32), // y coordinate
|
|
}
|
|
publicKey, _ = cbor.Marshal(coseKey)
|
|
case -257: // RS256
|
|
coseKey := map[int]any{
|
|
1: 3, // kty: RSA
|
|
3: -257, // alg: RS256
|
|
-1: make([]byte, 256), // n (modulus)
|
|
-2: []byte{1, 0, 1}, // e (exponent = 65537)
|
|
}
|
|
publicKey, _ = cbor.Marshal(coseKey)
|
|
case -8: // EdDSA
|
|
coseKey := map[int]any{
|
|
1: 1, // kty: OKP
|
|
3: -8, // alg: EdDSA
|
|
-1: 6, // crv: Ed25519
|
|
-2: make([]byte, 32), // x coordinate
|
|
}
|
|
publicKey, _ = cbor.Marshal(coseKey)
|
|
default: // Default to ES256
|
|
coseKey := map[int]any{
|
|
1: 2, // kty: EC2
|
|
3: -7, // alg: ES256
|
|
-1: 1, // crv: P-256
|
|
-2: make([]byte, 32), // x coordinate
|
|
-3: make([]byte, 32), // y coordinate
|
|
}
|
|
publicKey, _ = cbor.Marshal(coseKey)
|
|
algorithm = -7
|
|
}
|
|
|
|
return &keeper.WebAuthnRegistrationData{
|
|
CredentialID: base64.RawURLEncoding.EncodeToString([]byte(credentialID)),
|
|
RawID: base64.RawURLEncoding.EncodeToString([]byte(credentialID)),
|
|
ClientDataJSON: base64.RawURLEncoding.EncodeToString(clientDataJSON),
|
|
AttestationObject: base64.RawURLEncoding.EncodeToString(attestationObj),
|
|
Username: username,
|
|
PublicKey: publicKey,
|
|
Algorithm: algorithm,
|
|
Origin: "http://localhost:8080",
|
|
}
|
|
}
|
|
|
|
func createTestClientDataJSON(challenge, origin string) []byte {
|
|
// Create client data that matches WebAuthn format
|
|
clientData := map[string]any{
|
|
"type": "webauthn.create",
|
|
"challenge": challenge, // Keep challenge as-is, will be base64 encoded by caller
|
|
"origin": origin,
|
|
"crossOrigin": false,
|
|
}
|
|
data, _ := json.Marshal(clientData)
|
|
return data
|
|
}
|
|
|
|
func createTestAttestationObject(credentialID string) []byte {
|
|
// Create a proper CBOR attestation object with valid structure
|
|
|
|
// Create COSE public key for ES256
|
|
coseKey := map[int]any{
|
|
1: 2, // kty: EC2
|
|
3: -7, // alg: ES256
|
|
-1: 1, // crv: P-256
|
|
-2: make([]byte, 32), // x coordinate (dummy)
|
|
-3: make([]byte, 32), // y coordinate (dummy)
|
|
}
|
|
publicKeyCOSE, _ := cbor.Marshal(coseKey)
|
|
|
|
// Create authenticator data
|
|
authData := createValidAuthenticatorData([]byte(credentialID), publicKeyCOSE)
|
|
|
|
// Create attestation object
|
|
attestationObj := map[string]any{
|
|
"fmt": "none",
|
|
"attStmt": map[string]any{},
|
|
"authData": authData,
|
|
}
|
|
|
|
attestationObjCBOR, _ := cbor.Marshal(attestationObj)
|
|
return attestationObjCBOR
|
|
}
|
|
|
|
func createValidAuthenticatorData(credentialID, publicKey []byte) []byte {
|
|
// RP ID hash (32 bytes) - SHA256 of "localhost"
|
|
rpIDHash := sha256.Sum256([]byte("localhost"))
|
|
|
|
// Flags byte: UP=1, UV=1, AT=1 (0x45)
|
|
flags := byte(0x45)
|
|
|
|
// Sign count (4 bytes)
|
|
signCount := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(signCount, 0)
|
|
|
|
// Build authenticator data
|
|
authData := make([]byte, 0)
|
|
authData = append(authData, rpIDHash[:]...)
|
|
authData = append(authData, flags)
|
|
authData = append(authData, signCount...)
|
|
|
|
// Add attested credential data (since AT flag is set)
|
|
// AAGUID (16 bytes) - all zeros for testing
|
|
aaguid := make([]byte, 16)
|
|
authData = append(authData, aaguid...)
|
|
|
|
// Credential ID length (2 bytes)
|
|
credIDLen := make([]byte, 2)
|
|
binary.BigEndian.PutUint16(credIDLen, uint16(len(credentialID)))
|
|
authData = append(authData, credIDLen...)
|
|
|
|
// Credential ID
|
|
authData = append(authData, credentialID...)
|
|
|
|
// Public key
|
|
authData = append(authData, publicKey...)
|
|
|
|
return authData
|
|
}
|