Skip to content

Commit

Permalink
chore: add revoke and delete auth credential validation
Browse files Browse the repository at this point in the history
  • Loading branch information
martinsaporiti committed Dec 18, 2024
1 parent 872a80b commit 315eab6
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 34 deletions.
4 changes: 2 additions & 2 deletions internal/api/connections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ func TestServer_DeleteConnection(t *testing.T) {
OtherIdentifier: userDID2.String(),
Expiration: 0,
Version: 0,
RevNonce: 0,
RevNonce: 1,
CoreClaim: domain.CoreClaim{},
Status: nil,
})
Expand Down Expand Up @@ -475,7 +475,7 @@ func TestServer_RevokeConnectionCredentials(t *testing.T) {
OtherIdentifier: userDID.String(),
Expiration: 0,
Version: 0,
RevNonce: 0,
RevNonce: 1,
CoreClaim: domain.CoreClaim{},
Status: nil,
})
Expand Down
15 changes: 14 additions & 1 deletion internal/api/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,24 @@ import (

// DeleteCredential deletes a credential
func (s *Server) DeleteCredential(ctx context.Context, request DeleteCredentialRequestObject) (DeleteCredentialResponseObject, error) {
did, err := w3c.ParseDID(request.Identifier)
if err != nil {
return DeleteCredential400JSONResponse{N400JSONResponse{Message: err.Error()}}, nil
}

clID, err := uuid.Parse(request.Id)
if err != nil {
return DeleteCredential400JSONResponse{N400JSONResponse{"invalid claim id"}}, nil
}

err = s.claimService.Delete(ctx, clID)
err = s.claimService.Delete(ctx, did, clID)
if err != nil {
if errors.Is(err, services.ErrCredentialNotFound) {
return DeleteCredential400JSONResponse{N400JSONResponse{"The given credential does not exist"}}, nil
}
if errors.Is(err, services.ErrAuthCredentialCannotBeRevoked) {
return DeleteCredential400JSONResponse{N400JSONResponse{Message: "you can not delete this auth credential"}}, nil
}
return DeleteCredential500JSONResponse{N500JSONResponse{"There was an error deleting the credential"}}, nil
}

Expand Down Expand Up @@ -139,6 +147,11 @@ func (s *Server) RevokeCredential(ctx context.Context, request RevokeCredentialR
}}, nil
}

if errors.Is(err, services.ErrAuthCredentialCannotBeRevoked) {
return RevokeCredential400JSONResponse{N400JSONResponse{
Message: err.Error(),
}}, nil
}
return RevokeCredential500JSONResponse{N500JSONResponse{Message: err.Error()}}, nil
}
return RevokeCredential202JSONResponse{
Expand Down
50 changes: 26 additions & 24 deletions internal/api/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,22 @@ import (

func TestServer_RevokeClaim(t *testing.T) {
server := newTestServer(t, nil)

idStr := "did:polygonid:polygon:mumbai:2qM77fA6NGGWL9QEeb1dv2VA6wz5svcohgv61LZ7wB"
identity := &domain.Identity{
Identifier: idStr,
}
fixture := repositories.NewFixture(storage)
fixture.CreateIdentity(t, identity)
identity, err := server.Services.identity.Create(context.Background(), "http://privado-test", &ports.DIDCreationOptions{Method: core.DIDMethodIden3, Blockchain: core.Privado, Network: core.Main, KeyType: kms.KeyTypeBabyJubJub})
require.NoError(t, err)

idClaim, err := uuid.NewUUID()
require.NoError(t, err)
nonce := int64(123)
revNonce := domain.RevNonceUint64(nonce)

fixture.CreateClaim(t, &domain.Claim{
ID: idClaim,
Identifier: &idStr,
Issuer: idStr,
Identifier: &identity.Identifier,
Issuer: identity.Identifier,
SchemaHash: "ca938857241db9451ea329256b9c06e5",
SchemaURL: "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/auth.json-ld",
SchemaType: "AuthBJJCredential",
SchemaURL: "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v3.json",
SchemaType: "KYCAgeCredential",
OtherIdentifier: "",
Expiration: 0,
Version: 0,
Expand All @@ -58,17 +55,6 @@ func TestServer_RevokeClaim(t *testing.T) {
Status: nil,
})

query := repositories.ExecQueryParams{
Query: `INSERT INTO identity_mts (identifier, type) VALUES
($1, 0),
($1, 1),
($1, 2),
($1, 3)`,
Arguments: []interface{}{idStr},
}

fixture.ExecQuery(t, query)

handler := getHandler(context.Background(), server)

type expected struct {
Expand All @@ -88,7 +74,7 @@ func TestServer_RevokeClaim(t *testing.T) {
{
name: "No auth header",
auth: authWrong,
did: idStr,
did: identity.Identifier,
nonce: nonce,
expected: expected{
httpCode: http.StatusUnauthorized,
Expand All @@ -97,7 +83,7 @@ func TestServer_RevokeClaim(t *testing.T) {
{
name: "should revoke the credentials",
auth: authOk,
did: idStr,
did: identity.Identifier,
nonce: nonce,
expected: expected{
httpCode: 202,
Expand All @@ -109,7 +95,7 @@ func TestServer_RevokeClaim(t *testing.T) {
{
name: "should get an error wrong nonce",
auth: authOk,
did: idStr,
did: identity.Identifier,
nonce: int64(1231323),
expected: expected{
httpCode: 404,
Expand All @@ -130,6 +116,18 @@ func TestServer_RevokeClaim(t *testing.T) {
}},
},
},
{
name: "should get an error - cannot revoke auth credential",
auth: authOk,
did: identity.Identifier,
nonce: 0,
expected: expected{
httpCode: 400,
response: RevokeCredential400JSONResponse{N400JSONResponse{
Message: "auth credential cannot be revoked",
}},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
rr := httptest.NewRecorder()
Expand All @@ -145,6 +143,10 @@ func TestServer_RevokeClaim(t *testing.T) {
var response RevokeCredential202JSONResponse
assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
assert.Equal(t, v.Message, response.Message)
case RevokeCredential400JSONResponse:
var response RevokeCredential400JSONResponse
assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
assert.Equal(t, v.Message, response.Message)
case RevokeCredential404JSONResponse:
var response RevokeCredential404JSONResponse
assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
Expand Down
5 changes: 5 additions & 0 deletions internal/core/domain/claim.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,8 @@ func (c *Claim) GetCredentialStatus() (*verifiable.CredentialStatus, error) {
}
return cStatus, nil
}

// EqualToSchemaHash returns true if the claim has the same schema hash
func (c *Claim) EqualToSchemaHash(schemaHash string) bool {
return c.SchemaHash == schemaHash
}
1 change: 1 addition & 0 deletions internal/core/ports/claim_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ClaimRepository interface {
GetByRevocationNonce(ctx context.Context, conn db.Querier, identifier *w3c.DID, revocationNonce domain.RevNonceUint64) ([]*domain.Claim, error)
GetByIdAndIssuer(ctx context.Context, conn db.Querier, identifier *w3c.DID, claimID uuid.UUID) (*domain.Claim, error)
FindOneClaimBySchemaHash(ctx context.Context, conn db.Querier, subject *w3c.DID, schemaHash string) (*domain.Claim, error)
FindClaimsBySchemaHash(ctx context.Context, conn db.Querier, subject *w3c.DID, schemaHash string) ([]*domain.Claim, error)
GetAllByIssuerID(ctx context.Context, conn db.Querier, identifier w3c.DID, filter *ClaimsFilter) ([]*domain.Claim, uint, error)
GetNonRevokedByConnectionAndIssuerID(ctx context.Context, conn db.Querier, connID uuid.UUID, issuerID w3c.DID) ([]*domain.Claim, error)
GetAllByState(ctx context.Context, conn db.Querier, did *w3c.DID, state *merkletree.Hash) (claims []domain.Claim, err error)
Expand Down
4 changes: 2 additions & 2 deletions internal/core/ports/claim_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ type ClaimService interface {
GetAuthClaim(ctx context.Context, did *w3c.DID) (*domain.Claim, error)
GetAuthClaimForPublishing(ctx context.Context, did *w3c.DID, state string) (*domain.Claim, error)
UpdateClaimsMTPAndState(ctx context.Context, currentState *domain.IdentityState) error
Delete(ctx context.Context, id uuid.UUID) error
Delete(ctx context.Context, issuerDID *w3c.DID, id uuid.UUID) error
GetByStateIDWithMTPProof(ctx context.Context, did *w3c.DID, state string) ([]*domain.Claim, error)
GetAuthCredentials(ctx context.Context, identifier *w3c.DID) ([]*domain.Claim, error)
GetAuthCredentialWithPublicKey(ctx context.Context, identifier *w3c.DID, pubKey []byte) (*domain.Claim, error)
GetAuthCredentialByPublicKey(ctx context.Context, identifier *w3c.DID, pubKey []byte) (*domain.Claim, error)
}
87 changes: 83 additions & 4 deletions internal/core/services/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ var (
ErrUnsupportedDisplayMethodType = errors.New("unsupported display method type") // ErrUnsupportedDisplayMethodType means the display method type is not supported
ErrUnsupportedRefreshServiceType = errors.New("unsupported refresh service type") // ErrUnsupportedRefreshServiceType means the refresh service type is not supported
ErrWrongCredentialSubjectID = errors.New("wrong format for credential subject ID") // ErrWrongCredentialSubjectID means the credential subject ID is wrong
ErrAuthCredentialCannotBeRevoked = errors.New("auth credential cannot be revoked") // ErrAuthCredentialCannotBeRevoked means the credential cannot be revoked
)

type claim struct {
Expand Down Expand Up @@ -302,8 +303,33 @@ func (c *claim) RevokeAllFromConnection(ctx context.Context, connID uuid.UUID, i
})
}

func (c *claim) Delete(ctx context.Context, id uuid.UUID) error {
err := c.icRepo.Delete(ctx, c.storage.Pgx, id)
func (c *claim) Delete(ctx context.Context, issuerDID *w3c.DID, id uuid.UUID) error {
claim, err := c.icRepo.GetByIdAndIssuer(ctx, c.storage.Pgx, issuerDID, id)
if err != nil {
if errors.Is(err, repositories.ErrClaimDoesNotExist) {
return ErrCredentialNotFound
}
return err
}

claims := make([]*domain.Claim, 1)
claims[0] = claim

authHash, err := core.AuthSchemaHash.MarshalText()
if err != nil {
return err
}

// check if the nonce can be deleted
canBeRevoked, err := c.canRevokeNonce(ctx, issuerDID, c.storage.Pgx, claims, uint64(claim.RevNonce), string(authHash))
if err != nil {
return fmt.Errorf("error checking if the nonce can be revoked: %w", err)
}
if !canBeRevoked {
return ErrAuthCredentialCannotBeRevoked
}

err = c.icRepo.Delete(ctx, c.storage.Pgx, id)
if err != nil {
if errors.Is(err, repositories.ErrClaimDoesNotExist) {
return ErrCredentialNotFound
Expand Down Expand Up @@ -588,6 +614,10 @@ func (c *claim) GetByStateIDWithMTPProof(ctx context.Context, did *w3c.DID, stat
return c.icRepo.GetByStateIDWithMTPProof(ctx, c.storage.Pgx, did, state)
}

// GetAuthCredentials returns the auth credentials for the given identifier
// The auth credentials are the credentials that are used to sign other credentials
// The credentials can have mtp proof or not.
// The credentials are not revoked
func (c *claim) GetAuthCredentials(ctx context.Context, identifier *w3c.DID) ([]*domain.Claim, error) {
authHash, err := core.AuthSchemaHash.MarshalText()
if err != nil {
Expand All @@ -596,8 +626,8 @@ func (c *claim) GetAuthCredentials(ctx context.Context, identifier *w3c.DID) ([]
return c.icRepo.GetAuthCoreClaims(ctx, c.storage.Pgx, identifier, string(authHash))
}

// GetAuthCredentialWithPublicKey returns the auth credential with the given public key
func (c *claim) GetAuthCredentialWithPublicKey(ctx context.Context, identifier *w3c.DID, publicKey []byte) (*domain.Claim, error) {
// GetAuthCredentialByPublicKey returns the auth credential with the given public key
func (c *claim) GetAuthCredentialByPublicKey(ctx context.Context, identifier *w3c.DID, publicKey []byte) (*domain.Claim, error) {
authCredentials, err := c.GetAuthCredentials(ctx, identifier)
if err != nil {
log.Error(ctx, "failed to get auth credentials", "err", err)
Expand All @@ -612,6 +642,26 @@ func (c *claim) GetAuthCredentialWithPublicKey(ctx context.Context, identifier *
}

func (c *claim) revoke(ctx context.Context, did *w3c.DID, nonce uint64, description string, querier db.Querier) error {
authHash, err := core.AuthSchemaHash.MarshalText()
if err != nil {
return err
}

// get the claims to revoke by nonce
claimsToRevoke, err := c.icRepo.GetByRevocationNonce(ctx, querier, did, domain.RevNonceUint64(nonce))
if err != nil {
log.Error(ctx, "error getting the claim by revocation nonce", "err", err)
}

// check if the nonce can be revoked
canBeRevoked, err := c.canRevokeNonce(ctx, did, querier, claimsToRevoke, nonce, string(authHash))
if err != nil {
return fmt.Errorf("error checking if the nonce can be revoked: %w", err)
}
if !canBeRevoked {
return ErrAuthCredentialCannotBeRevoked
}

rID := new(big.Int).SetUint64(nonce)
revocation := domain.Revocation{
Identifier: did.String(),
Expand Down Expand Up @@ -661,6 +711,35 @@ func (c *claim) revoke(ctx context.Context, did *w3c.DID, nonce uint64, descript
return nil
}

// canRevokeNonce checks if the nonce can be revoked
func (c *claim) canRevokeNonce(ctx context.Context, did *w3c.DID, querier db.Querier, claimsToRevoke []*domain.Claim, nonce uint64, authHash string) (bool, error) {
checkAuthCredentials := false
for _, claim := range claimsToRevoke {
if claim.EqualToSchemaHash(authHash) {
checkAuthCredentials = true
break
}
}

canBeRevoked := true
if checkAuthCredentials {
authCredentials, err := c.icRepo.FindClaimsBySchemaHash(ctx, querier, did, authHash)
if err != nil {
return false, fmt.Errorf("error getting the auth credentials: %w", err)
}
if len(authCredentials) == 1 {
return false, nil
}
canBeRevoked = false
for _, authCredential := range authCredentials {
if authCredential.RevNonce != domain.RevNonceUint64(nonce) {
canBeRevoked = true
}
}
}
return canBeRevoked, nil
}

func (c *claim) getRevocationStatus(ctx context.Context, basicMessage *ports.AgentRequest) (*domain.Agent, error) {
revData := &protocol.RevocationStatusRequestMessageBody{}
err := json.Unmarshal(basicMessage.Body, revData)
Expand Down
2 changes: 1 addition & 1 deletion internal/core/services/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func getKeyType(keyID string) (kms.KeyType, error) {
// hasAssociatedAuthCredential checks if the bbj key has an associated auth credential
func (ks *Key) hasAssociatedAuthCredential(ctx context.Context, did *w3c.DID, publicKey []byte) (bool, *domain.Claim, error) {
hasAssociatedAuthCredential := false
authCredential, err := ks.claimService.GetAuthCredentialWithPublicKey(ctx, did, publicKey)
authCredential, err := ks.claimService.GetAuthCredentialByPublicKey(ctx, did, publicKey)
if err != nil {
log.Error(ctx, "failed to check if key has associated auth credential", "err", err)
return false, nil, err
Expand Down
Loading

0 comments on commit 315eab6

Please sign in to comment.