Commit b73b7df8 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Merge branch 'optional-attributes'

parents f4262fdf 6b0a56ad
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"time" "time"
"fmt" "fmt"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/mhe/gabi" "github.com/mhe/gabi"
) )
...@@ -38,8 +39,6 @@ const ( ...@@ -38,8 +39,6 @@ const (
) )
var ( var (
metadataVersion = []byte{0x02}
versionField = metadataField{1, 0} versionField = metadataField{1, 0}
signingDateField = metadataField{3, 1} signingDateField = metadataField{3, 1}
validityField = metadataField{2, 4} validityField = metadataField{2, 4}
...@@ -102,7 +101,15 @@ func (al *AttributeList) Strings() []TranslatedString { ...@@ -102,7 +101,15 @@ func (al *AttributeList) Strings() []TranslatedString {
if al.strings == nil { if al.strings == nil {
al.strings = make([]TranslatedString, len(al.Ints)-1) al.strings = make([]TranslatedString, len(al.Ints)-1)
for index, num := range al.Ints[1:] { // skip metadata for index, num := range al.Ints[1:] { // skip metadata
al.strings[index] = map[string]string{"en": string(num.Bytes()), "nl": string(num.Bytes())} // TODO bi := new(big.Int).Set(num)
if al.MetadataAttribute.Version() >= 3 {
if bi.Bit(0) == 0 { // attribute does not exist
continue
}
bi = bi.Rsh(bi, 1)
}
val := string(bi.Bytes())
al.strings[index] = map[string]string{"en": val, "nl": val} // TODO
} }
} }
return al.strings return al.strings
...@@ -139,13 +146,13 @@ func MetadataFromInt(i *big.Int, conf *Configuration) *MetadataAttribute { ...@@ -139,13 +146,13 @@ func MetadataFromInt(i *big.Int, conf *Configuration) *MetadataAttribute {
} }
// NewMetadataAttribute constructs a new instance containing the default values: // NewMetadataAttribute constructs a new instance containing the default values:
// 0x02 as versionField // provided version as versionField
// now as signing date // now as signing date
// 0 as keycounter // 0 as keycounter
// ValidityDefault (half a year) as default validity. // ValidityDefault (half a year) as default validity.
func NewMetadataAttribute() *MetadataAttribute { func NewMetadataAttribute(version byte) *MetadataAttribute {
val := MetadataAttribute{new(big.Int), nil, nil} val := MetadataAttribute{new(big.Int), nil, nil}
val.setField(versionField, metadataVersion) val.setField(versionField, []byte{version})
val.setSigningDate() val.setSigningDate()
val.setKeyCounter(0) val.setKeyCounter(0)
val.setDefaultValidityDuration() val.setDefaultValidityDuration()
......
...@@ -31,7 +31,6 @@ func NewCredentialInfo(ints []*big.Int, conf *Configuration) *CredentialInfo { ...@@ -31,7 +31,6 @@ func NewCredentialInfo(ints []*big.Int, conf *Configuration) *CredentialInfo {
} }
attrs := NewAttributeListFromInts(ints, conf) attrs := NewAttributeListFromInts(ints, conf)
id := credtype.Identifier() id := credtype.Identifier()
issid := id.IssuerIdentifier() issid := id.IssuerIdentifier()
return &CredentialInfo{ return &CredentialInfo{
......
...@@ -77,6 +77,7 @@ func (ct *CredentialType) ContainsAttribute(ai AttributeTypeIdentifier) bool { ...@@ -77,6 +77,7 @@ func (ct *CredentialType) ContainsAttribute(ai AttributeTypeIdentifier) bool {
// AttributeDescription is a description of an attribute within a credential type. // AttributeDescription is a description of an attribute within a credential type.
type AttributeDescription struct { type AttributeDescription struct {
ID string `xml:"id,attr"` ID string `xml:"id,attr"`
Optional string `xml:"optional,attr"`
Name TranslatedString Name TranslatedString
Description TranslatedString Description TranslatedString
} }
......
...@@ -594,7 +594,7 @@ func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, re ...@@ -594,7 +594,7 @@ func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, re
// we save none of them to fail the session cleanly // we save none of them to fail the session cleanly
gabicreds := []*gabi.Credential{} gabicreds := []*gabi.Credential{}
for i, sig := range msg { for i, sig := range msg {
attrs, err := request.Credentials[i].AttributeList(client.Configuration) attrs, err := request.Credentials[i].AttributeList(client.Configuration, getMetadataVersion(request.GetVersion()))
if err != nil { if err != nil {
return err return err
} }
......
...@@ -54,7 +54,7 @@ func (session *session) createLogEntry(response interface{}) (*LogEntry, error) ...@@ -54,7 +54,7 @@ func (session *session) createLogEntry(response interface{}) (*LogEntry, error)
entry.Received = map[irma.CredentialTypeIdentifier][]irma.TranslatedString{} entry.Received = map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
} }
for _, req := range session.jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials { for _, req := range session.jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials {
list, err := req.AttributeList(session.client.Configuration) list, err := req.AttributeList(session.client.Configuration, getMetadataVersion(session.Version))
if err != nil { if err != nil {
continue // TODO? continue // TODO?
} }
......
...@@ -50,10 +50,19 @@ type SessionDismisser interface { ...@@ -50,10 +50,19 @@ type SessionDismisser interface {
Dismiss() Dismiss()
} }
// getMetadataVersion maps a chosen protocol version to a metadata version that
// the server will use.
func getMetadataVersion(v *irma.ProtocolVersion) byte {
if v.Below(2, 3) {
return 0x02 // no support for optional attributes
}
return 0x03 // current version
}
type session struct { type session struct {
Action irma.Action Action irma.Action
Handler Handler Handler Handler
Version irma.Version Version *irma.ProtocolVersion
choice *irma.DisclosureChoice choice *irma.DisclosureChoice
client *Client client *Client
...@@ -73,24 +82,24 @@ var _ keyshareSessionHandler = (*session)(nil) ...@@ -73,24 +82,24 @@ var _ keyshareSessionHandler = (*session)(nil)
// Supported protocol versions. Minor version numbers should be reverse sorted. // Supported protocol versions. Minor version numbers should be reverse sorted.
var supportedVersions = map[int][]int{ var supportedVersions = map[int][]int{
2: {2, 1}, 2: {3, 2, 1},
} }
func calcVersion(qr *irma.Qr) (string, error) { func calcVersion(qr *irma.Qr) (*irma.ProtocolVersion, error) {
// Parse range supported by server // Parse range supported by server
var minmajor, minminor, maxmajor, maxminor int var minmajor, minminor, maxmajor, maxminor int
var err error var err error
if minmajor, err = strconv.Atoi(string(qr.ProtocolVersion[0])); err != nil { if minmajor, err = strconv.Atoi(string(qr.ProtocolVersion[0])); err != nil {
return "", err return nil, err
} }
if minminor, err = strconv.Atoi(string(qr.ProtocolVersion[2])); err != nil { if minminor, err = strconv.Atoi(string(qr.ProtocolVersion[2])); err != nil {
return "", err return nil, err
} }
if maxmajor, err = strconv.Atoi(string(qr.ProtocolMaxVersion[0])); err != nil { if maxmajor, err = strconv.Atoi(string(qr.ProtocolMaxVersion[0])); err != nil {
return "", err return nil, err
} }
if maxminor, err = strconv.Atoi(string(qr.ProtocolMaxVersion[2])); err != nil { if maxminor, err = strconv.Atoi(string(qr.ProtocolMaxVersion[2])); err != nil {
return "", err return nil, err
} }
// Iterate supportedVersions in reverse sorted order (i.e. biggest major number first) // Iterate supportedVersions in reverse sorted order (i.e. biggest major number first)
...@@ -104,11 +113,11 @@ func calcVersion(qr *irma.Qr) (string, error) { ...@@ -104,11 +113,11 @@ func calcVersion(qr *irma.Qr) (string, error) {
aboveMinimum := major > minmajor || (major == minmajor && minor >= minminor) aboveMinimum := major > minmajor || (major == minmajor && minor >= minminor)
underMaximum := major < maxmajor || (major == maxmajor && minor <= maxminor) underMaximum := major < maxmajor || (major == maxmajor && minor <= maxminor)
if aboveMinimum && underMaximum { if aboveMinimum && underMaximum {
return fmt.Sprintf("%d.%d", major, minor), nil return irma.NewVersion(major, minor), nil
} }
} }
} }
return "", fmt.Errorf("No supported protocol version between %s and %s", qr.ProtocolVersion, qr.ProtocolMaxVersion) return nil, fmt.Errorf("No supported protocol version between %s and %s", qr.ProtocolVersion, qr.ProtocolMaxVersion)
} }
func (session *session) IsInteractive() bool { func (session *session) IsInteractive() bool {
...@@ -220,7 +229,7 @@ func (client *Client) NewManualSession(sigrequestJSONString string, handler Hand ...@@ -220,7 +229,7 @@ func (client *Client) NewManualSession(sigrequestJSONString string, handler Hand
Action: irma.ActionSigning, // TODO hardcoded for now Action: irma.ActionSigning, // TODO hardcoded for now
Handler: handler, Handler: handler,
client: client, client: client,
Version: irma.Version("2"), // TODO hardcoded for now Version: irma.NewVersion(2, 0), // TODO hardcoded for now
irmaSession: sigrequest, irmaSession: sigrequest,
} }
...@@ -268,7 +277,8 @@ func (client *Client) NewSession(qr *irma.Qr, handler Handler) SessionDismisser ...@@ -268,7 +277,8 @@ func (client *Client) NewSession(qr *irma.Qr, handler Handler) SessionDismisser
session.fail(&irma.SessionError{ErrorType: irma.ErrorProtocolVersionNotSupported, Err: err}) session.fail(&irma.SessionError{ErrorType: irma.ErrorProtocolVersionNotSupported, Err: err})
return nil return nil
} }
session.Version = irma.Version(version) session.Version = version
session.transport.SetHeader("X-IRMA-ProtocolVersion", version.String())
// Check if the action is one of the supported types // Check if the action is one of the supported types
switch session.Action { switch session.Action {
...@@ -315,6 +325,7 @@ func (session *session) start() { ...@@ -315,6 +325,7 @@ func (session *session) start() {
session.irmaSession = session.jwt.IrmaSession() session.irmaSession = session.jwt.IrmaSession()
session.irmaSession.SetContext(session.info.Context) session.irmaSession.SetContext(session.info.Context)
session.irmaSession.SetNonce(session.info.Nonce) session.irmaSession.SetNonce(session.info.Nonce)
session.irmaSession.SetVersion(session.Version)
if session.Action == irma.ActionIssuing { if session.Action == irma.ActionIssuing {
ir := session.irmaSession.(*irma.IssuanceRequest) ir := session.irmaSession.(*irma.IssuanceRequest)
// Store which public keys the server will use // Store which public keys the server will use
...@@ -330,7 +341,7 @@ func (session *session) start() { ...@@ -330,7 +341,7 @@ func (session *session) start() {
if session.Action == irma.ActionIssuing { if session.Action == irma.ActionIssuing {
ir := session.irmaSession.(*irma.IssuanceRequest) ir := session.irmaSession.(*irma.IssuanceRequest)
for _, credreq := range ir.Credentials { for _, credreq := range ir.Credentials {
info, err := credreq.Info(session.client.Configuration) info, err := credreq.Info(session.client.Configuration, getMetadataVersion(session.Version))
if err != nil { if err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownCredentialType, Err: err}) session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownCredentialType, Err: err})
return return
......
...@@ -137,6 +137,27 @@ func getIssuanceRequest(defaultValidity bool) *irma.IssuanceRequest { ...@@ -137,6 +137,27 @@ func getIssuanceRequest(defaultValidity bool) *irma.IssuanceRequest {
} }
} }
func getNameIssuanceRequest() *irma.IssuanceRequest {
expiry := irma.Timestamp(irma.NewMetadataAttribute(0).Expiry())
credid := irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.fullName")
req := &irma.IssuanceRequest{
Credentials: []*irma.CredentialRequest{
{
Validity: &expiry,
CredentialTypeID: &credid,
Attributes: map[string]string{
"firstnames": "Johan Pieter",
"firstname": "Johan",
"familyname": "Stuivezand",
},
},
},
}
return req
}
func getIssuanceJwt(name string, defaultValidity bool) interface{} { func getIssuanceJwt(name string, defaultValidity bool) interface{} {
return irma.NewIdentityProviderJwt(name, getIssuanceRequest(defaultValidity)) return irma.NewIdentityProviderJwt(name, getIssuanceRequest(defaultValidity))
} }
...@@ -191,6 +212,26 @@ func TestDefaultCredentialValidity(t *testing.T) { ...@@ -191,6 +212,26 @@ func TestDefaultCredentialValidity(t *testing.T) {
sessionHelper(t, jwtcontents, "issue", client) sessionHelper(t, jwtcontents, "issue", client)
} }
func TestIssuanceOptionalEmptyAttributes(t *testing.T) {
req := getNameIssuanceRequest()
jwt := irma.NewIdentityProviderJwt("testip", req)
sessionHelper(t, jwt, "issue", nil)
}
func TestIssuanceOptionalZeroLengthAttributes(t *testing.T) {
req := getNameIssuanceRequest()
req.Credentials[0].Attributes["prefix"] = ""
jwt := irma.NewIdentityProviderJwt("testip", req)
sessionHelper(t, jwt, "issue", nil)
}
func TestIssuanceOptionalSetAttributes(t *testing.T) {
req := getNameIssuanceRequest()
req.Credentials[0].Attributes["prefix"] = "van"
jwt := irma.NewIdentityProviderJwt("testip", req)
sessionHelper(t, jwt, "issue", nil)
}
func TestLargeAttribute(t *testing.T) { func TestLargeAttribute(t *testing.T) {
client := parseStorage(t) client := parseStorage(t)
require.NoError(t, client.RemoveAllCredentials()) require.NoError(t, client.RemoveAllCredentials())
...@@ -268,7 +309,7 @@ func TestKeyshareEnrollmentAndSessions(t *testing.T) { ...@@ -268,7 +309,7 @@ func TestKeyshareEnrollmentAndSessions(t *testing.T) {
enrollKeyshareServer(t, client) enrollKeyshareServer(t, client)
id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
expiry := irma.Timestamp(irma.NewMetadataAttribute().Expiry()) expiry := irma.Timestamp(irma.NewMetadataAttribute(0).Expiry())
credid := irma.NewCredentialTypeIdentifier("test.test.mijnirma") credid := irma.NewCredentialTypeIdentifier("test.test.mijnirma")
jwt := getCombinedJwt("testip", id) jwt := getCombinedJwt("testip", id)
jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials = append( jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials = append(
...@@ -311,7 +352,7 @@ func TestKeyshareSessions(t *testing.T) { ...@@ -311,7 +352,7 @@ func TestKeyshareSessions(t *testing.T) {
client := parseStorage(t) client := parseStorage(t)
id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
expiry := irma.Timestamp(irma.NewMetadataAttribute().Expiry()) expiry := irma.Timestamp(irma.NewMetadataAttribute(0).Expiry())
credid := irma.NewCredentialTypeIdentifier("test.test.mijnirma") credid := irma.NewCredentialTypeIdentifier("test.test.mijnirma")
jwt := getCombinedJwt("testip", id) jwt := getCombinedJwt("testip", id)
jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials = append( jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials = append(
......
...@@ -166,7 +166,7 @@ func TestAttributeDisjunctionMarshaling(t *testing.T) { ...@@ -166,7 +166,7 @@ func TestAttributeDisjunctionMarshaling(t *testing.T) {
} }
func TestMetadataAttribute(t *testing.T) { func TestMetadataAttribute(t *testing.T) {
metadata := NewMetadataAttribute() metadata := NewMetadataAttribute(0x02)
if metadata.Version() != 0x02 { if metadata.Version() != 0x02 {
t.Errorf("Unexpected metadata version: %d", metadata.Version()) t.Errorf("Unexpected metadata version: %d", metadata.Version())
} }
......
...@@ -16,8 +16,27 @@ import ( ...@@ -16,8 +16,27 @@ import (
// Status encodes the status of an IRMA session (e.g., connected). // Status encodes the status of an IRMA session (e.g., connected).
type Status string type Status string
// Version encodes the IRMA protocol version of an IRMA session. // ProtocolVersion encodes the IRMA protocol version of an IRMA session.
type Version string type ProtocolVersion struct {
major int
minor int
}
func NewVersion(major, minor int) *ProtocolVersion {
return &ProtocolVersion{major, minor}
}
func (v *ProtocolVersion) String() string {
return fmt.Sprintf("%d.%d", v.major, v.minor)
}
// Returns true if v is below the given version.
func (v *ProtocolVersion) Below(major, minor int) bool {
if v.major < major {
return true
}
return v.major == major && v.minor < minor
}
// Action encodes the session type of an IRMA session (e.g., disclosing). // Action encodes the session type of an IRMA session (e.g., disclosing).
type Action string type Action string
......
...@@ -20,6 +20,8 @@ type SessionRequest struct { ...@@ -20,6 +20,8 @@ type SessionRequest struct {
Choice *DisclosureChoice `json:"-"` Choice *DisclosureChoice `json:"-"`
Ids *IrmaIdentifierSet `json:"-"` Ids *IrmaIdentifierSet `json:"-"`
version *ProtocolVersion
} }
func (sr *SessionRequest) SetCandidates(candidates [][]*AttributeIdentifier) { func (sr *SessionRequest) SetCandidates(candidates [][]*AttributeIdentifier) {
...@@ -36,6 +38,16 @@ func (sr *SessionRequest) SetDisclosureChoice(choice *DisclosureChoice) { ...@@ -36,6 +38,16 @@ func (sr *SessionRequest) SetDisclosureChoice(choice *DisclosureChoice) {
sr.Choice = choice sr.Choice = choice
} }
// ...
func (sr *SessionRequest) SetVersion(v *ProtocolVersion) {
sr.version = v
}
// ...
func (sr *SessionRequest) GetVersion() *ProtocolVersion {
return sr.version
}
// A DisclosureRequest is a request to disclose certain attributes. // A DisclosureRequest is a request to disclose certain attributes.
type DisclosureRequest struct { type DisclosureRequest struct {
SessionRequest SessionRequest
...@@ -113,6 +125,7 @@ type IrmaSession interface { ...@@ -113,6 +125,7 @@ type IrmaSession interface {
SetNonce(*big.Int) SetNonce(*big.Int)
GetContext() *big.Int GetContext() *big.Int
SetContext(*big.Int) SetContext(*big.Int)
SetVersion(*ProtocolVersion)
ToDisclose() AttributeDisjunctionList ToDisclose() AttributeDisjunctionList
DisclosureChoice() *DisclosureChoice DisclosureChoice() *DisclosureChoice
SetDisclosureChoice(choice *DisclosureChoice) SetDisclosureChoice(choice *DisclosureChoice)
...@@ -123,8 +136,8 @@ type IrmaSession interface { ...@@ -123,8 +136,8 @@ type IrmaSession interface {
// Timestamp is a time.Time that marshals to Unix timestamps. // Timestamp is a time.Time that marshals to Unix timestamps.
type Timestamp time.Time type Timestamp time.Time
func (cr *CredentialRequest) Info(conf *Configuration) (*CredentialInfo, error) { func (cr *CredentialRequest) Info(conf *Configuration, metadataVersion byte) (*CredentialInfo, error) {
list, err := cr.AttributeList(conf) list, err := cr.AttributeList(conf, metadataVersion)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -132,8 +145,8 @@ func (cr *CredentialRequest) Info(conf *Configuration) (*CredentialInfo, error) ...@@ -132,8 +145,8 @@ func (cr *CredentialRequest) Info(conf *Configuration) (*CredentialInfo, error)
} }
// AttributeList returns the list of attributes from this credential request. // AttributeList returns the list of attributes from this credential request.
func (cr *CredentialRequest) AttributeList(conf *Configuration) (*AttributeList, error) { func (cr *CredentialRequest) AttributeList(conf *Configuration, metadataVersion byte) (*AttributeList, error) {
meta := NewMetadataAttribute() meta := NewMetadataAttribute(metadataVersion)
meta.setKeyCounter(cr.KeyCounter) meta.setKeyCounter(cr.KeyCounter)
meta.setCredentialTypeIdentifier(cr.CredentialTypeID.String()) meta.setCredentialTypeIdentifier(cr.CredentialTypeID.String())
meta.setSigningDate() meta.setSigningDate()
...@@ -142,21 +155,41 @@ func (cr *CredentialRequest) AttributeList(conf *Configuration) (*AttributeList, ...@@ -142,21 +155,41 @@ func (cr *CredentialRequest) AttributeList(conf *Configuration) (*AttributeList,
return nil, err return nil, err
} }
attrs := make([]*big.Int, len(cr.Attributes)+1, len(cr.Attributes)+1)
credtype := conf.CredentialTypes[*cr.CredentialTypeID] credtype := conf.CredentialTypes[*cr.CredentialTypeID]
if credtype == nil { if credtype == nil {
return nil, errors.New("Unknown credential type") return nil, errors.New("Unknown credential type")
} }
if len(credtype.Attributes) != len(cr.Attributes) {
return nil, errors.New("Received unexpected amount of attributes") // Check that there are no attributes in the credential request that aren't
// in the credential descriptor.
for crName := range cr.Attributes {
found := false
for _, ad := range credtype.Attributes {
if ad.ID == crName {
found = true
break
}
}
if !found {
return nil, errors.New("Unknown CR attribute")
}
} }
attrs := make([]*big.Int, len(credtype.Attributes)+1)
attrs[0] = meta.Int attrs[0] = meta.Int
for i, attrtype := range credtype.Attributes { for i, attrtype := range credtype.Attributes {
attrs[i+1] = new(big.Int)
if str, present := cr.Attributes[attrtype.ID]; present { if str, present := cr.Attributes[attrtype.ID]; present {
attrs[i+1] = new(big.Int).SetBytes([]byte(str)) // Set attribute to str << 1 + 1
attrs[i+1].SetBytes([]byte(str))
if meta.Version() >= 0x03 {
attrs[i+1].Lsh(attrs[i+1], 1) // attr <<= 1
attrs[i+1].Add(attrs[i+1], big.NewInt(1)) // attr += 1
}
} else { } else {
return nil, errors.New("Unknown attribute") if (attrtype.Optional != "true") {
return nil, errors.New("Required attribute not provided")
}
} }
} }
......
<IssueSpecification version="4">
<Name>
<en>Name</en>
<nl>Naam</nl>
</Name>
<ShortName>
<en>Name</en>
<nl>Naam</nl>
</ShortName>
<SchemeManager>irma-demo</SchemeManager>
<IssuerID>MijnOverheid</IssuerID>
<CredentialID>fullName</CredentialID>
<Description>
<en>Your full name, as it is known to the government</en>
<nl>Uw volledige naam, zoals bekend bij de overheid</nl>
</Description>
<ShouldBeSingleton>true</ShouldBeSingleton>
<Attributes>
<Attribute id="firstnames">
<Name>
<en>First names</en>
<nl>Voornamen</nl>
</Name>
<Description>
<en>All of your first names</en>
<nl>Al uw voornamen</nl>
</Description>
</Attribute>
<Attribute id="firstname">
<Name>
<en>First name</en>
<nl>Voornaam</nl>
</Name>
<Description>
<en>Your first name</en>
<nl>Uw voornaam</nl>
</Description>
</Attribute>
<Attribute id="familyname">
<Name>
<en>Family name</en>
<nl>Achternaam</nl>
</Name>
<Description>
<en>Your family name</en>
<nl>Uw achternaam</nl>
</Description>
</Attribute>
<Attribute id="prefix" optional="true">
<Name>
<en>Prefix</en>
<nl>Tussenvoegsel</nl>
</Name>
<Description>
<en>Family name prefix</en>
<nl>Tussenvoegsel van uw achternaam</nl>