nuts-foundation/nuts-node

View on GitHub
vcr/revocation/types.go

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
/*
 * Copyright (C) 2024 Nuts community
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package revocation

import (
    "context"
    "errors"
    "fmt"
    "net/url"
    "strconv"
    "time"

    "github.com/nuts-foundation/go-did"
    "github.com/nuts-foundation/go-did/did"
    "github.com/nuts-foundation/go-did/vc"
    "github.com/nuts-foundation/nuts-node/core"
    "github.com/nuts-foundation/nuts-node/crypto"
    "github.com/nuts-foundation/nuts-node/jsonld"
    "github.com/nuts-foundation/nuts-node/vcr/types"
    "gorm.io/gorm"
)

const (
    // StatusList2021CredentialType is the type of StatusList2021Credential
    StatusList2021CredentialType = "StatusList2021Credential"
    // StatusList2021CredentialSubjectType is the credentialSubject.type in a StatusList2021Credential
    StatusList2021CredentialSubjectType = "StatusList2021"
    // StatusList2021EntryType is the credentialStatus.type
    StatusList2021EntryType = "StatusList2021Entry"
)

var StatusList2021ContextURI = ssi.MustParseURI(jsonld.W3cStatusList2021Context)
var statusList2021CredentialTypeURI = ssi.MustParseURI(StatusList2021CredentialType)

// errNotFound wraps types.ErrNotFound to clarify which StatusList2021Credential is not found
var errNotFound = fmt.Errorf("status list: %w", types.ErrNotFound)

// errRevoked wraps types.ErrRevoked to clarify the source of the error
var errRevoked = fmt.Errorf("status list: %w", types.ErrRevoked)

// errUnsupportedPurpose limits current usage to 'revocation'
var errUnsupportedPurpose = errors.New("status list: purpose not supported")

type StatusPurpose string

const (
    StatusPurposeRevocation = "revocation"
    statusPurposeSuspension = "suspension" // currently not supported
)

// StatusList2021Issuer is the issuer side of StatusList2021
type StatusList2021Issuer interface {
    // Credential provides a valid StatusList2021Credential with subject ID derived from the issuer and page.
    // It returns the last issued StatusList2021Credential if it is still valid or issues a new StatusList2021Credential.
    Credential(ctx context.Context, issuer did.DID, page int) (*vc.VerifiableCredential, error)
    // Entry creates a StatusList2021Entry that can be added to the credentialStatus of a VC.
    // The corresponding StatusList2021Credential will have a gap in the bitstring if the returned entry does not make it into a VC.
    // If the entry belongs to a new StatusList2021Credential, an empty StatusList2021Credential is issued and stored.
    Entry(ctx context.Context, issuer did.DID, purpose StatusPurpose) (*StatusList2021Entry, error)
    // Revoke by adding the StatusList2021Entry to the list of revocations, and updates the relevant StatusList2021Credential.
    // The credentialID allows reverse search of revocations, its issuer is NOT verified against the entry issuer or VC.
    // Returns types.ErrRevoked if already revoked, or types.ErrNotFound when the entry.StatusListCredential is unknown.
    Revoke(ctx context.Context, credentialID ssi.URI, entry StatusList2021Entry) error
}

// StatusList2021Verifier is the verifier side of StatusList2021
type StatusList2021Verifier interface {
    // Verify returns a types.ErrRevoked when the credentialStatus contains a 'StatusList2021Entry' that can be resolved and lists the credential as 'revoked'
    // Other credentialStatus type/statusPurpose are ignored. Verification may fail with other non-standardized errors.
    Verify(credentialToVerify vc.VerifiableCredential) error
}

// VerifySignFn verifies the signature on VC. The vcr.verifier injects its VerifySignature method here.
type VerifySignFn func(credentialToVerify vc.VerifiableCredential, validateAt *time.Time) error

// SignFn signs a VC according to the specified format. The vcr.issuer injects its signVC method here.
type SignFn func(ctx context.Context, unsignedCredential vc.VerifiableCredential, key crypto.Key) (*vc.VerifiableCredential, error)

// ResolveKeyFn resolves the key used SignFn to sign the StatusList2021Credential.
// The vcr.issuer injects its keyResolver.ResolveAssertionKey here.
type ResolveKeyFn func(ctx context.Context, issuerDID did.DID) (crypto.Key, error)

var _ StatusList2021Issuer = (*StatusList2021)(nil)
var _ StatusList2021Verifier = (*StatusList2021)(nil)

// StatusList2021 implements the W3C Verifiable Credentials Status List v2021 draft specification.
// https://www.w3.org/TR/2023/WD-vc-status-list-20230427/
// VerifySignature and Sign methods are used to verify and sign StatusList2021Credentials
type StatusList2021 struct {
    client          core.HTTPRequestDoer
    db              *gorm.DB
    baseURL         string
    VerifySignature VerifySignFn // injected by verifier
    Sign            SignFn       // injected by issuer, context must contain an audit log
    ResolveKey      ResolveKeyFn // injected by issuer
}

// NewStatusList2021 returns a StatusList2021 without a Sign or VerifySignature method.
// The URL in the credential will be constructed as follows using the given base URL: <baseURL>/statuslist/<did>/<page>
func NewStatusList2021(db *gorm.DB, client core.HTTPRequestDoer, baseURL string) *StatusList2021 {
    return &StatusList2021{client: client, db: db, baseURL: baseURL}
}

// StatusList2021Entry is the "credentialStatus" property used by issuers to enable VerifiableCredential status information.
type StatusList2021Entry struct {
    // ID is expected to be a URL that identifies the status information associated with the verifiable credential.
    // It MUST NOT be the URL for the status list, which is in StatusListCredential.
    ID string `json:"id,omitempty"`
    // Type MUST be "StatusList2021Entry"
    Type string `json:"type,omitempty"`
    // StatusPurpose indicates what it means if the VerifiableCredential is on the list.
    // The value is arbitrary, with predefined values `revocation` and `suspension`.
    // This value must match credentialSubject.statusPurpose value in the VerifiableCredential.
    StatusPurpose string `json:"statusPurpose,omitempty"`
    // StatusListIndex is an arbitrary size integer greater than or equal to 0, expressed as a string.
    // The value identifies the position in the bitstring of the corresponding StatusListCredential.
    StatusListIndex string `json:"statusListIndex,omitempty"`
    // StatusListCredential property MUST be a URL to the StatusList2021Credential.
    // When the URL is dereferenced, the resulting verifiable credential's type MUST include the "StatusList2021Credential".
    StatusListCredential string `json:"statusListCredential,omitempty"`
}

// Validate returns an error if the contents of the StatusList2021Entry violate the spec.
func (e StatusList2021Entry) Validate() error {
    // 'id' MUST NOT be the URL for the status list
    if e.ID == e.StatusListCredential {
        return errors.New("StatusList2021Entry.id is the same as the StatusList2021Entry.statusListCredential")
    }

    if e.Type != StatusList2021EntryType {
        return errors.New("StatusList2021Entry.type must be StatusList2021Entry")
    }

    // StatusPurpose must contain a purpose
    if e.StatusPurpose == "" {
        return errors.New("StatusList2021Entry.statusPurpose is required")
    }

    // statusListIndex must be a non-negative number
    if n, err := strconv.Atoi(e.StatusListIndex); err != nil || n < 0 {
        return errors.New("invalid StatusList2021Entry.statusListIndex")
    }

    // 'statusListCredential' must be a URL
    if _, err := url.ParseRequestURI(e.StatusListCredential); err != nil {
        return fmt.Errorf("parse StatusList2021Entry.statusListCredential URL: %w", err)
    }

    return nil
}

// StatusList2021CredentialSubject of a StatusList2021Credential
type StatusList2021CredentialSubject struct {
    // ID for the credential subject
    ID string `json:"id"`
    // Type MUST be "StatusList2021Credential"
    Type string `json:"type"`
    // StatusPurpose defines the reason credentials are listed. ('revocation', 'suspension')
    StatusPurpose string `json:"statusPurpose"`
    // EncodedList is the GZIP-compressed [RFC1952], base-64 encoded [RFC4648] bitstring values for the associated range
    // of verifiable credential status values. The uncompressed bitstring MUST be at least 16KB in size.
    EncodedList string `json:"encodedList"`
}