nuts-foundation/nuts-auth

View on GitHub
pkg/services/irma/validator.go

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * Nuts auth
 * Copyright (C) 2020. 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 irma

import (
    "encoding/base64"
    "encoding/json"
    "errors"
    "fmt"
    "time"

    "github.com/nuts-foundation/nuts-auth/logging"

    irmaserver2 "github.com/privacybydesign/irmago/server/irmaserver"

    "github.com/nuts-foundation/nuts-auth/pkg/services"

    "github.com/nuts-foundation/nuts-auth/pkg/contract"

    core "github.com/nuts-foundation/nuts-go-core"

    "github.com/dgrijalva/jwt-go"
    nutscrypto "github.com/nuts-foundation/nuts-crypto/pkg"
    cryptoTypes "github.com/nuts-foundation/nuts-crypto/pkg/types"
    registry "github.com/nuts-foundation/nuts-registry/pkg"
    irma "github.com/privacybydesign/irmago"
    irmaserver "github.com/privacybydesign/irmago/server"
)

// todo rename to verifier

// VerifiablePresentationType is the irma verifiable presentation type
const VerifiablePresentationType = contract.VPType("NutsIrmaPresentation")

// ContractFormat holds the readable identifier of this signing means.
const ContractFormat = contract.SigningMeans("irma")

// Service validates contracts using the IRMA logic.
type Service struct {
    IrmaSessionHandler SessionHandler
    IrmaConfig         *irma.Configuration
    IrmaServiceConfig  ValidatorConfig
    // todo: remove this when the deprecated ValidateJwt is removed
    Registry          registry.RegistryClient
    Crypto            nutscrypto.Client
    ContractTemplates contract.TemplateStore
}

// ValidatorConfig holds the configuration for the irma validator.
type ValidatorConfig struct {
    // Address to bind the http server to. Default localhost:1323
    Address string
    // PublicURL is used for discovery for the IRMA app.
    PublicURL string
    // Where to find the IrmaConfig files including the schemas
    IrmaConfigPath string
    // Which scheme manager to use
    IrmaSchemeManager string
    // Auto update the schemas every x minutes or not?
    SkipAutoUpdateIrmaSchemas bool
}

// IsInitialized is a helper function to determine if the validator has been initialized properly.
func (v Service) IsInitialized() bool {
    return v.IrmaConfig != nil
}

// VerifiablePresentation is a specific proof for irma signatures
type VerifiablePresentation struct {
    contract.VerifiablePresentationBase
    Proof VPProof `json:"proof"`
}

// VPProof is a specific IrmaProof for the specific VerifiablePresentation
type VPProof struct {
    contract.Proof
    ProofValue string `json:"proofValue"`
}

// VerifyVP expects the given raw VerifiablePresentation to be of the correct type
// todo: type check?
func (v Service) VerifyVP(rawVerifiablePresentation []byte, checkTime *time.Time) (*contract.VPVerificationResult, error) {
    // Extract the Irma message
    vp := VerifiablePresentation{}
    if err := json.Unmarshal(rawVerifiablePresentation, &vp); err != nil {
        return nil, fmt.Errorf("could not verify VP: %w", err)
    }

    // Create the irma contract validator
    contractValidator := contractVerifier{v.IrmaConfig, v.ContractTemplates}
    signedContract, err := contractValidator.Parse(vp.Proof.ProofValue)
    if err != nil {
        return nil, err
    }

    cvr, err := contractValidator.verifyAll(signedContract.(*SignedIrmaContract), nil, checkTime)
    if err != nil {
        return nil, err
    }

    signerAttributes, err := signedContract.SignerAttributes()
    if err != nil {
        return nil, fmt.Errorf("could not verify vp: could not get signer attributes: %w", err)
    }

    return &contract.VPVerificationResult{
        Validity:            contract.State(cvr.ValidationResult),
        VPType:              contract.VPType(cvr.ContractFormat),
        DisclosedAttributes: signerAttributes,
        ContractAttributes:  signedContract.Contract().Params,
    }, nil
}

// ValidateContract is the entry point for contract validation.
// It decodes the base64 encoded contract, parses the contract string, and validates the contract.
// Returns nil, ErrUnknownContractFormat if the contract used in the message is unknown
// deprecated
func (v Service) ValidateContract(b64EncodedContract string, format services.ContractFormat, actingPartyCN *string, checkTime *time.Time) (*services.ContractValidationResult, error) {
    if format == services.IrmaFormat {
        contract, err := base64.StdEncoding.DecodeString(b64EncodedContract)
        if err != nil {
            return nil, fmt.Errorf("could not base64-decode contract: %w", err)
        }
        // Create the irma contract validator
        contractValidator := contractVerifier{v.IrmaConfig, v.ContractTemplates}
        signedContract, err := contractValidator.ParseIrmaContract(contract)
        if err != nil {
            return nil, err
        }
        return contractValidator.verifyAll(signedContract.(*SignedIrmaContract), actingPartyCN, checkTime)
    }
    return nil, contract.ErrUnknownContractFormat
}

// ValidateJwt validates a JWT formatted identity token
// deprecated
func (v Service) ValidateJwt(rawJwt string, actingPartyCN *string, checkTime *time.Time) (*services.ContractValidationResult, error) {
    parser := &jwt.Parser{ValidMethods: services.ValidJWTAlg}
    parsedToken, err := parser.ParseWithClaims(rawJwt, &services.NutsIdentityToken{}, func(token *jwt.Token) (i interface{}, e error) {
        legalEntity, err := parseTokenIssuer(token.Claims.(*services.NutsIdentityToken).Issuer)
        if err != nil {
            return nil, err
        }

        // get public key
        org, err := v.Registry.OrganizationById(legalEntity)
        if err != nil {
            return nil, err
        }

        pk, err := org.CurrentPublicKey()
        if err != nil {
            return nil, err
        }

        var key interface{}
        err = pk.Raw(&key)
        return key, err
    })

    if err != nil {
        return nil, err
    }

    claims := parsedToken.Claims.(*services.NutsIdentityToken)

    if claims.Type != services.IrmaFormat {
        return nil, fmt.Errorf("%s: %w", claims.Type, contract.ErrInvalidContractFormat)
    }

    contractStr, err := base64.StdEncoding.DecodeString(claims.Signature)
    if err != nil {
        return nil, err
    }

    // Create the irma contract validator
    contractValidator := contractVerifier{v.IrmaConfig, v.ContractTemplates}
    signedContract, err := contractValidator.ParseIrmaContract(contractStr)
    if err != nil {
        return nil, err
    }
    return contractValidator.verifyAll(signedContract.(*SignedIrmaContract), actingPartyCN, checkTime)
}

// SessionStatus returns the current status of a certain session.
// It returns nil if the session is not found
// deprecated
func (v Service) SessionStatus(id services.SessionID) (*services.SessionStatusResult, error) {
    if result := v.IrmaSessionHandler.GetSessionResult(string(id)); result != nil {
        var (
            token string
        )
        if result.Signature != nil {
            c, err := contract.ParseContractString(result.Signature.Message, v.ContractTemplates)
            sic := &SignedIrmaContract{
                IrmaContract: *result.Signature,
                contract:     c,
            }
            if err != nil {
                return nil, err
            }

            le, err := v.legalEntityFromContract(sic)
            if err != nil {
                return nil, fmt.Errorf("could not create JWT for given session: %w", err)
            }

            token, err = v.CreateIdentityTokenFromIrmaContract(sic, le)
            if err != nil {
                return nil, err
            }
        }
        result := &services.SessionStatusResult{SessionResult: *result, NutsAuthToken: token}
        logging.Log().Info(result.NutsAuthToken)
        return result, nil
    }
    return nil, services.ErrSessionNotFound
}

func (v Service) legalEntityFromContract(sic *SignedIrmaContract) (core.PartyID, error) {
    params := sic.contract.Params

    if _, ok := params[contract.LegalEntityAttr]; !ok {
        return core.PartyID{}, ErrLegalEntityNotProvided
    }

    le, err := v.Registry.ReverseLookup(params[contract.LegalEntityAttr])
    if err != nil {
        return core.PartyID{}, err
    }

    return le.Identifier, nil
}

// CreateIdentityTokenFromIrmaContract from a signed irma contract. Returns a JWT signed with the provided legalEntity.
func (v Service) CreateIdentityTokenFromIrmaContract(contract *SignedIrmaContract, legalEntity core.PartyID) (string, error) {
    signature, err := json.Marshal(contract.IrmaContract)
    encodedSignature := base64.StdEncoding.EncodeToString(signature)
    if err != nil {
        return "", err
    }
    payload := services.NutsIdentityToken{
        StandardClaims: jwt.StandardClaims{
            Issuer: legalEntity.String(),
        },
        Signature: encodedSignature,
        Type:      services.IrmaFormat,
    }

    claims, err := convertPayloadToClaims(payload)
    if err != nil {
        return "", fmt.Errorf("could not construct claims: %w", err)
    }

    tokenString, err := v.Crypto.SignJWT(claims, cryptoTypes.KeyForEntity(cryptoTypes.LegalEntity{URI: legalEntity.String()}))
    if err != nil {
        return "", fmt.Errorf("could not sign jwt: %w", err)
    }
    return tokenString, nil
}

// convertPayloadToClaims converts a nutsJwt struct to a map of strings so it can be signed with the crypto module
func convertPayloadToClaims(payload services.NutsIdentityToken) (map[string]interface{}, error) {

    var (
        jsonString []byte
        err        error
        claims     map[string]interface{}
    )

    if jsonString, err = json.Marshal(payload); err != nil {
        return nil, fmt.Errorf("could not marshall payload: %w", err)
    }

    if err := json.Unmarshal(jsonString, &claims); err != nil {
        return nil, fmt.Errorf("could not unmarshal string: %w", err)
    }

    return claims, nil
}

// StartSession starts an irma session.
// This is mainly a wrapper around the irma.SessionHandler.StartSession
func (v Service) StartSession(request interface{}, handler irmaserver.SessionHandler) (*irma.Qr, string, error) {
    return v.IrmaSessionHandler.StartSession(request, handler)
}

func parseTokenIssuer(issuer string) (core.PartyID, error) {
    if issuer == "" {
        return core.PartyID{}, ErrLegalEntityNotProvided
    }
    result, err := core.ParsePartyID(issuer)
    if err != nil {
        return core.PartyID{}, fmt.Errorf("invalid token issuer: %w", err)
    }
    return result, nil
}

// SessionHandler is an abstraction for the Irma Server, mainly for enabling better testing
type SessionHandler interface {
    GetSessionResult(token string) *irmaserver.SessionResult
    StartSession(request interface{}, handler irmaserver.SessionHandler) (*irma.Qr, string, error)
}

// Compile time check if the DefaultIrmaSessionHandler implements the SessionHandler interface
var _ SessionHandler = (*DefaultIrmaSessionHandler)(nil)

// DefaultIrmaSessionHandler is a wrapper for the Irma Server
// It implements the SessionHandler interface
type DefaultIrmaSessionHandler struct {
    I *irmaserver2.Server
}

// GetSessionResult forwards to Irma Server instance
func (d *DefaultIrmaSessionHandler) GetSessionResult(token string) *irmaserver.SessionResult {
    return d.I.GetSessionResult(token)
}

// StartSession forwards to Irma Server instance
func (d *DefaultIrmaSessionHandler) StartSession(request interface{}, handler irmaserver.SessionHandler) (*irma.Qr, string, error) {
    return d.I.StartSession(request, handler)
}

// ErrLegalEntityNotProvided indicates that the legalEntity is missing
var ErrLegalEntityNotProvided = errors.New("legalEntity not provided")