pkg/services/irma/irmacontract.go
/*
* 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
}