nuts-foundation/nuts-auth

View on GitHub
pkg/services/irma/irmacontract.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"
    "github.com/nuts-foundation/nuts-auth/logging"
    "strings"
    "time"

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

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

    core "github.com/nuts-foundation/nuts-go-core"
    irma "github.com/privacybydesign/irmago"
)

// SignedIrmaContract holds the contract and additional methods to parse and validate.
type SignedIrmaContract struct {
    IrmaContract irma.SignedMessage
    contract     *contract.Contract
    attributes   map[string]string
    // Cached proofStatus because attribute extraction and signature validation is performed during parsing
    proofStatus irma.ProofStatus
}

// SignerAttributes returns a map of irma attributes minus the root:
// {
//   "gemeente.personalData.fullname": "Henk de Vries",
//   "sidn-pbdf.email.email": "henk.devries@example.com",
// },
func (s SignedIrmaContract) SignerAttributes() (map[string]string, error) {
    return s.attributes, nil
}

// Contract returns the signed contract.Contract by the irma contract
func (s SignedIrmaContract) Contract() contract.Contract {
    return *s.contract
}

// A IrmaContract is valid when:
//  it has a valid signature
//  it contains a message that is a known Contract
//  its signature is signed with all attributes required by the Contract
//  it has a valid time period
//  the acting party named in the contract is the same as the one making the request
type contractVerifier struct {
    irmaConfig     *irma.Configuration
    validContracts contract.TemplateStore
}

// Parse an IRMA Authentication Token. A token is a base64 encoded IRMA contract.
func (cv *contractVerifier) Parse(rawAuthToken string) (services.SignedToken, error) {
    decodedAuthToken, err := base64.StdEncoding.DecodeString(rawAuthToken)
    if err != nil {
        return nil, fmt.Errorf("unable to Parse VP: %w", err)
    }
    return cv.ParseIrmaContract(decodedAuthToken)
}

// ParseIrmaContract accepts a json encoded irma contract and performs the following operations/validations:
// * Checks the irma signature
// * Parses the signed attributes
// * Checks if the contract message is set
// * Parses the contract from the message
// Returns a signedIrmaContract
// Note that the irma contract validation is performed during the parsing phase.
// This is because parsing and attribute extraction is done in one step.
func (cv *contractVerifier) ParseIrmaContract(jsonIrmaContract []byte) (services.SignedToken, error) {
    signedIrmaContract := &SignedIrmaContract{}

    if err := json.Unmarshal(jsonIrmaContract, &signedIrmaContract.IrmaContract); err != nil {
        return nil, fmt.Errorf("could not parse IRMA contract: %w", err)
    }

    if signedIrmaContract.IrmaContract.Message == "" {
        return nil, fmt.Errorf("could not parse contract: empty message")
    }

    attributes, status, err := signedIrmaContract.IrmaContract.Verify(cv.irmaConfig, nil)
    signerAttributes := parseSignerAttributes(attributes)

    contractMessage := signedIrmaContract.IrmaContract.Message
    c, err := contract.ParseContractString(contractMessage, cv.validContracts)
    if err != nil {
        return nil, err
    }

    signedIrmaContract.contract = c
    signedIrmaContract.proofStatus = status
    signedIrmaContract.attributes = signerAttributes

    return signedIrmaContract, nil
}

// Verify checks if the SignedIrmaContract:
// * Has a valid irma signature (stored in proofStatus during parsing)
// * Has a valid contract
// Returns an error if one of the checks fails
func (cv *contractVerifier) Verify(token services.SignedToken) error {
    irmaToken, ok := token.(SignedIrmaContract)
    if !ok {
        return errors.New("could not verify token: could not cast token to SignedIrmaToken")
    }
    if irmaToken.proofStatus != irma.ProofStatusValid {
        return fmt.Errorf("irma proof invalid: %s", irmaToken.proofStatus)
    }
    return token.Contract().Verify()
}

// parseSignedIrmaContract parses a json string containing a signed irma contract.
func (cv *contractVerifier) parseSignedIrmaContract(rawContract string) (*SignedIrmaContract, error) {
    signedToken, err := cv.Parse(rawContract)
    if err != nil {
        return nil, err
    }
    return signedToken.(*SignedIrmaContract), nil
}

func parseSignerAttributes(attributes [][]*irma.DisclosedAttribute) map[string]string {
    if len(attributes) == 0 {
        return map[string]string{}
    }
    // take the attributes rawvalue and add them to a list.
    disclosedAttributes := make(map[string]string, len(attributes[0]))
    strictMode := core.NutsConfig().InStrictMode()
    for _, att := range attributes[0] {
        // Check schemaManager. Only the pdbf root is accepted in strictMode.
        schemaManager := att.Identifier.Root()
        if strictMode && schemaManager != "pbdf" {
            logging.Log().Infof("IRMA schemeManager %s is not valid in strictMode", schemaManager)
            continue
        }
        identifier := att.Identifier.String()
        // strip of the schemeManager
        if i := strings.Index(identifier, "."); i != -1 {
            identifier = identifier[i+1:]
        }
        disclosedAttributes[identifier] = *att.RawValue
    }
    return disclosedAttributes
}

// verifyAll verifies the contract contents, the signer attributes and the proof status and returns a ContractValidationResult
// It can be used by both the old JWT verifier and the new VPVerifier
func (cv *contractVerifier) verifyAll(signedContract *SignedIrmaContract, actingPartyCn *string, checkTime *time.Time) (*services.ContractValidationResult, error) {
    res := &services.ContractValidationResult{
        ContractFormat: services.IrmaFormat,
    }

    if signedContract.proofStatus == irma.ProofStatusValid {
        res.ValidationResult = services.Valid
        res.DisclosedAttributes = signedContract.attributes
    } else {
        res.ValidationResult = services.Invalid
    }

    var err error
    res, err = cv.validateContractContents(signedContract, res, actingPartyCn, checkTime)
    if err != nil {
        return nil, err
    }
    return cv.verifyRequiredAttributes(signedContract, res)
}

// validateContractContents validates at the actual contract contents.
// Is the timeframe valid and does the common name corresponds with the contract message.
func (cv *contractVerifier) validateContractContents(signedContract *SignedIrmaContract, validationResult *services.ContractValidationResult, actingPartyCn *string, checkTimeP *time.Time) (*services.ContractValidationResult, error) {
    if validationResult.ValidationResult == services.Invalid {
        return validationResult, nil
    }

    checkTime := time.Now()
    if checkTimeP != nil {
        checkTime = *checkTimeP
    }
    // Validate time frame
    if err := signedContract.contract.VerifyForGivenTime(checkTime); err != nil {
        validationResult.ValidationResult = services.Invalid
        return validationResult, nil
    }

    // Validate ActingParty Common Name
    // todo remove in 0.17
    if actingPartyCn != nil {
        ok, err := validateActingParty(signedContract.contract.Params, *actingPartyCn)
        if err != nil {
            return nil, err
        }
        if !ok {
            validationResult.ValidationResult = services.Invalid
            return validationResult, nil
        }
    }

    // all valid fill contractAttributes
    validationResult.ContractAttributes = signedContract.contract.Params

    return validationResult, nil
}

// ValidateActingParty checks if an given actingParty is equal to the actingParty in a contract
// Usually the given acting party comes from the CN of the client-certificate. This check verifies if the
// contract was actual created by the acting party. This prevents the reuse of the contract by another party.
func validateActingParty(params map[string]string, actingParty string) (bool, error) {
    // If no acting party is given, that is probably an implementation error.
    if actingParty == "" {
        return false, errors.New("actingParty validation failed: actingParty cannot be empty")
    }

    // if no acting party in the params, error
    actingPartyFromContract, ok := params[contract.ActingPartyAttr]
    if !ok {
        return false, errors.New("actingParty validation failed: no acting party found in contract params")
    }

    // perform the actual check
    return actingParty == actingPartyFromContract, nil
}

// verifyRequiredAttributes checks if all attributes required by a contract template are actually present in the signature
func (cv *contractVerifier) verifyRequiredAttributes(signedIrmaContract *SignedIrmaContract, validationResult *services.ContractValidationResult) (*services.ContractValidationResult, error) {
    if validationResult.ValidationResult == services.Invalid {
        return validationResult, nil
    }

    contractTemplate := signedIrmaContract.contract.Template

    // use a map to ignore duplicates. Allows us to compare lengths
    validationRes := make(map[string]bool)

    requiredAttributes := contractTemplate.SignerAttributes

    for disclosedAtt := range validationResult.DisclosedAttributes {
        // e.g. gemeente.personalData.firstnames
        for _, requiredAttribute := range requiredAttributes {
            // e.g. .gemeente.personalData.firstnames
            if strings.HasSuffix(requiredAttribute, disclosedAtt) {
                validationRes[requiredAttribute] = true
            }
        }
    }

    if len(validationRes) != len(requiredAttributes) {
        foundAttributes := make([]string, len(validationRes))
        for k := range validationRes {
            foundAttributes = append(foundAttributes, k)
        }

        disclosedAttributes := make([]string, len(validationResult.DisclosedAttributes))
        for k := range validationResult.DisclosedAttributes {
            disclosedAttributes = append(disclosedAttributes, k)
        }
        validationResult.ValidationResult = services.Invalid
        logging.Log().Infof("missing required attributes in signature. found: %v, needed: %v, disclosed: %v", foundAttributes, requiredAttributes, disclosedAttributes)
    }

    return validationResult, nil
}