nuts-foundation/nuts-node

View on GitHub
auth/services/dummy/dummy.go

Summary

Maintainability
A
0 mins
Test Coverage
C
75%
/*
 * Nuts node
 * Copyright (C) 2021 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 dummy

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "encoding/json"
    "errors"
    "fmt"
    "time"

    ssi "github.com/nuts-foundation/go-did"
    "github.com/nuts-foundation/go-did/vc"
    "github.com/nuts-foundation/nuts-node/auth/contract"
    "github.com/nuts-foundation/nuts-node/auth/services"
)

// ContractFormat is the contract format type
const ContractFormat = "dummy"

// VerifiablePresentationType is the dummy verifiable presentation type
const VerifiablePresentationType = "DummyVerifiablePresentation"

// NoSignatureType is a VerifiablePresentation Proof type where no signature is given
const NoSignatureType = "NoSignature"

// SessionCreated represents the session state after creation
const SessionCreated = "created"

// SessionInProgress represents the session state after the first SessionStatus call
const SessionInProgress = "in-progress"

// SessionCompleted represents the session state after the second SessionStatus call
const SessionCompleted = "completed"

var errNotEnabled = errors.New("not allowed in strict mode")

// Dummy is a contract signer and verifier that always succeeds unless you try to use it in strict mode
// The dummy signer is not supposed to be used in a clustered context unless consecutive calls arrive at the same instance
type Dummy struct {
    InStrictMode bool
    Sessions     map[string]string
    Status       map[string]string
}

// Proof holds the Proof generated from the dummy Signer
type Proof struct {
    // Proof type, mandatory
    Type string
    // Contract as how it was presented to the user
    Contract string
    // FamilyName from the signing means
    FamilyName string
    // GivenName from the signing means
    Initials string
    // Prefix from the signing means
    Prefix string
    // Email from the signing means
    Email string
}

// SignedToken is the Dummy implementation of a Signed token.
// It can be used in the dummy.Service service.
type SignedToken struct {
    signerAttributes map[string]string
    contract         contract.Contract
}

// SignerAttributes returns the attributes used to sign the token
func (d SignedToken) SignerAttributes() (map[string]string, error) {
    return d.signerAttributes, nil
}

// Contract returns the contract
func (d SignedToken) Contract() contract.Contract {
    return d.contract
}

// sessionPointer contains a information to facilitate session discoverability for the signing means
type sessionPointer struct {
    sessionID string
}

// SessionID returns a string which can be used by the signing means to find the session
func (s sessionPointer) SessionID() string {
    return s.sessionID
}

// Payload returns always the dummy value
func (s sessionPointer) Payload() []byte {
    return []byte("dummy")
}

func (s sessionPointer) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        SessionID string `json:"sessionID"`
    }{SessionID: s.sessionID})
}

type dummyVPVerificationResult struct {
    disclosedAttributes map[string]string
    contractAttributes  map[string]string
}

func (d dummyVPVerificationResult) Validity() contract.State {
    return contract.Valid
}

func (d dummyVPVerificationResult) Reason() string {
    return ""
}

func (d dummyVPVerificationResult) VPType() string {
    return VerifiablePresentationType
}

func (d dummyVPVerificationResult) DisclosedAttribute(key string) string {
    return d.disclosedAttributes[key]
}

func (d dummyVPVerificationResult) ContractAttribute(key string) string {
    return d.contractAttributes[key]
}

func (d dummyVPVerificationResult) DisclosedAttributes() map[string]string {
    return d.disclosedAttributes
}

func (d dummyVPVerificationResult) ContractAttributes() map[string]string {
    return d.contractAttributes
}

type signingSessionResult struct {
    ID      string
    State   string
    Request string
}

// Status returns the current state of the signing session
func (d signingSessionResult) Status() string {
    return d.State
}

// VerifiablePresentation returns the contract.VerifiablePresentation if the session is completed, nil otherwise.
func (d signingSessionResult) VerifiablePresentation() (*vc.VerifiablePresentation, error) {
    // todo: the contract template should be used to select the dummy attributes to add

    if d.Status() != SessionCompleted {
        return nil, nil
    }

    return &vc.VerifiablePresentation{
        Context: []ssi.URI{vc.VCContextV1URI()},
        Type:    []ssi.URI{vc.VerifiablePresentationTypeV1URI(), ssi.MustParseURI(VerifiablePresentationType)},
        Proof: []interface{}{
            Proof{
                Type:       NoSignatureType,
                Initials:   "I",
                Prefix:     "von",
                FamilyName: "Dummy",
                Email:      "tester@example.com",
                Contract:   d.Request},
        },
    }, nil
}

func (d Dummy) Start(_ context.Context) {
}

// VerifyVP check a Dummy VerifiablePresentation. It Returns a verificationResult if all was fine, an error otherwise.
func (d Dummy) VerifyVP(vp vc.VerifiablePresentation, _ *time.Time) (contract.VPVerificationResult, error) {
    if d.InStrictMode {
        return nil, errNotEnabled
    }

    proofs := make([]Proof, 0)
    if err := vp.UnmarshalProofValue(&proofs); err != nil {
        return nil, err
    }

    if len(proofs) != 1 {
        return nil, fmt.Errorf("invalid number of proofs in Dummy proof: %v", proofs)
    }

    proof := proofs[0]
    c, err := contract.ParseContractString(proof.Contract, contract.StandardContractTemplates)
    if err != nil {
        return nil, err
    }

    // follows openid default claims
    return dummyVPVerificationResult{
        disclosedAttributes: map[string]string{
            services.InitialsTokenClaim:   proof.Initials,
            services.PrefixTokenClaim:     proof.Prefix,
            services.FamilyNameTokenClaim: proof.FamilyName,
            services.EmailTokenClaim:      proof.Email,
            services.UsernameClaim:        proof.Email,
            services.AssuranceLevelClaim:  "low",
        },
        contractAttributes: c.Params,
    }, nil
}

// SigningSessionStatus looks up the session by the provided sessionID param.
// When the session exists it returns the current state and advances the state to the next one.
// When the session is SessionComplete, it removes the session from the sessionStore.
func (d Dummy) SigningSessionStatus(_ context.Context, sessionID string) (contract.SigningSessionResult, error) {
    if d.InStrictMode {
        return nil, errNotEnabled
    }

    state, ok := d.Status[sessionID]
    if !ok {
        return nil, services.ErrSessionNotFound
    }

    session, ok := d.Sessions[sessionID]
    if !ok {
        return nil, services.ErrSessionNotFound
    }

    // increase session status everytime this request is made
    switch state {
    case SessionCreated:
        d.Status[sessionID] = SessionInProgress
    case SessionInProgress:
        d.Status[sessionID] = SessionCompleted
    case SessionCompleted:
        delete(d.Status, sessionID)
        delete(d.Sessions, sessionID)
    }
    return signingSessionResult{
        ID:      sessionID,
        State:   state,
        Request: session,
    }, nil
}

// StartSigningSession starts a Dummy session. It takes any string and stores it under a random sessionID.
// This method is not available in strictMode
// returns the sessionPointer with the sessionID
func (d Dummy) StartSigningSession(contract contract.Contract, params map[string]interface{}) (contract.SessionPointer, error) {
    if d.InStrictMode {
        return nil, errNotEnabled
    }
    sessionBytes := make([]byte, 16)
    _, _ = rand.Reader.Read(sessionBytes)

    sessionID := hex.EncodeToString(sessionBytes)
    d.Status[sessionID] = SessionCreated
    d.Sessions[sessionID] = contract.RawContractText

    return sessionPointer{
        sessionID: sessionID,
    }, nil
}