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

Move metastore & descriptions from gabi

parent 972085b9
......@@ -2,24 +2,56 @@ package irmago
import (
"crypto/sha256"
"encoding/binary"
"math/big"
"time"
"github.com/mhe/gabi"
)
const (
// ExpiryFactor is the precision for the expiry attribute. Value is one week.
ExpiryFactor = 60 * 60 * 24 * 7
// ValidityDefault is the default validity of new credentials (half a year).
ValidityDefault = 52 / 2
metadataLength = 1 + 3 + 2 + 2 + 16
)
var (
metadataVersion = []byte{0x02}
versionField = MetadataField{1, 0}
signingDateField = MetadataField{3, 1}
validityField = MetadataField{2, 4}
keyCounterField = MetadataField{2, 6}
credentialID = MetadataField{16, 8}
)
// MetadataField contains the length and offset of a field within a metadata attribute.
type MetadataField struct {
length int
offset int
}
// MetadataAttribute represent a metadata attribute. Contains the credential type, signing date, validity, and the public key counter.
type MetadataAttribute struct {
Int *big.Int
pk *gabi.PublicKey
}
// AttributeList contains attributes, excluding the secret key,
// providing convenient access to the metadata attribute.
type AttributeList struct {
Ints []*big.Int
strings []string
*gabi.MetadataAttribute `json:"-"`
Ints []*big.Int
strings []string
*MetadataAttribute `json:"-"`
}
// NewAttributeListFromInts initializes a new AttributeList from a list of bigints.
func NewAttributeListFromInts(ints []*big.Int) *AttributeList {
return &AttributeList{
Ints: ints,
MetadataAttribute: gabi.MetadataFromInt(ints[0]),
MetadataAttribute: MetadataFromInt(ints[0]),
}
}
......@@ -43,3 +75,137 @@ func (al *AttributeList) Strings() []string {
}
return al.strings
}
// MetadataFromInt wraps the given Int
func MetadataFromInt(i *big.Int) *MetadataAttribute {
return &MetadataAttribute{Int: i}
}
// NewMetadataAttribute constructs a new instance containing the default values:
// 0x02 as versionField
// now as signing date
// 0 as keycounter
// ValidityDefault (half a year) as default validity.
func NewMetadataAttribute() *MetadataAttribute {
val := MetadataAttribute{new(big.Int), nil}
val.setField(versionField, metadataVersion)
val.setSigningDate()
val.setKeyCounter(0)
val.setValidityDuration(ValidityDefault)
return &val
}
// Bytes returns this metadata attribute as a byte slice.
func (attr *MetadataAttribute) Bytes() []byte {
bytes := attr.Int.Bytes()
if len(bytes) < metadataLength {
bytes = append(bytes, make([]byte, metadataLength-len(bytes))...)
}
return bytes
}
// PublicKey extracts identifier of the Idemix public key with which this instance was signed,
// and returns this public key.
func (attr *MetadataAttribute) PublicKey() *gabi.PublicKey {
if attr.pk == nil {
attr.pk = MetaStore.PublicKey(attr.CredentialType().IssuerIdentifier(), attr.KeyCounter())
}
return attr.pk
}
// Version returns the metadata version of this instance
func (attr *MetadataAttribute) Version() byte {
return attr.field(versionField)[0]
}
// SigningDate returns the time at which this instance was signed
func (attr *MetadataAttribute) SigningDate() time.Time {
bytes := attr.field(signingDateField)
bytes = bytes[1:] // The signing date field is one byte too long
timestamp := int64(binary.BigEndian.Uint16(bytes)) * ExpiryFactor
return time.Unix(timestamp, 0)
}
func (attr *MetadataAttribute) setSigningDate() {
attr.setField(signingDateField, shortToByte(int(time.Now().Unix()/ExpiryFactor)))
}
// KeyCounter return the public key counter of the metadata attribute
func (attr *MetadataAttribute) KeyCounter() int {
return int(binary.BigEndian.Uint16(attr.field(keyCounterField)))
}
func (attr *MetadataAttribute) setKeyCounter(i int) {
attr.setField(keyCounterField, shortToByte(i))
}
// ValidityDuration returns the amount of epochs during which this instance is valid
func (attr *MetadataAttribute) ValidityDuration() int {
return int(binary.BigEndian.Uint16(attr.field(validityField)))
}
func (attr *MetadataAttribute) setValidityDuration(weeks int) {
attr.setField(validityField, shortToByte(weeks))
}
// CredentialType returns the credential type of the current instance
// using the MetaStore.
func (attr *MetadataAttribute) CredentialType() *CredentialType {
return MetaStore.hashToCredentialType(attr.field(credentialID))
}
func (attr *MetadataAttribute) setCredentialIdentifier(id string) {
bytes := sha256.Sum256([]byte(id))
attr.setField(credentialID, bytes[:16])
}
// Expiry returns the expiry date of this instance
func (attr *MetadataAttribute) Expiry() time.Time {
expiry := attr.SigningDate().Unix() + int64(attr.ValidityDuration()*ExpiryFactor)
return time.Unix(expiry, 0)
}
// IsValidOn returns whether this instance is still valid at the given time
func (attr *MetadataAttribute) IsValidOn(t time.Time) bool {
return attr.Expiry().After(t)
}
// IsValid returns whether this instance is valid.
func (attr *MetadataAttribute) IsValid() bool {
return attr.IsValidOn(time.Now())
}
func (attr *MetadataAttribute) field(field MetadataField) []byte {
return attr.Bytes()[field.offset : field.offset+field.length]
}
func (attr *MetadataAttribute) setField(field MetadataField, value []byte) {
if len(value) > field.length {
panic("Specified metadata field too large")
}
bytes := attr.Bytes()
// Push the value to the right within the field. Graphical representation:
// --xxxXXX----
// "-" indicates a byte of another field
// "X" is a byte of the value and "x" of our field
// In this example, our field has offset 2, length 6,
// but the specified value is only 3 bytes long.
startindex := field.length - len(value)
for i := 0; i < field.length; i++ {
if i < startindex {
bytes[i+field.offset] = 0
} else {
bytes[i+field.offset] = value[i-startindex]
}
}
attr.Int.SetBytes(bytes)
}
func shortToByte(x int) []byte {
bytes := make([]byte, 2)
binary.BigEndian.PutUint16(bytes, uint16(x))
return bytes
}
package irmago
import "github.com/mhe/gabi"
// Credential represents an IRMA credential, whose zeroth attribute
// is always the secret key and the first attribute the metadata attribute.
type Credential struct {
*gabi.Credential
*MetadataAttribute
}
func newCredential(gabicred *gabi.Credential) (cred *Credential) {
cred = &Credential{}
cred.Credential = gabicred
cred.MetadataAttribute = MetadataFromInt(gabicred.Attributes[1])
return
}
package irmago
import (
"encoding/xml"
"github.com/mhe/gabi"
)
// SchemeManager describes a scheme manager.
type SchemeManager struct {
Name string `xml:"Id"`
URL string `xml:"Contact"`
HRName TranslatedString `xml:"Name"`
Description TranslatedString
KeyshareServer string
KeyshareWebsite string
KeyshareAttribute string
XMLVersion int `xml:"version,attr"`
XMLName xml.Name `xml:"SchemeManager"`
}
// Issuer describes an issuer.
type Issuer struct {
HRName TranslatedString `xml:"Name"`
HRShortName TranslatedString `xml:"ShortName"`
Name string `xml:"ID"`
SchemeManagerName string `xml:"SchemeManager"`
ContactAddress string
ContactEMail string
URL string `xml:"baseURL"`
XMLVersion int `xml:"version,attr"`
}
// CredentialType is a description of a credential type, specifying (a.o.) its name, issuer, and attributes.
type CredentialType struct {
HRName TranslatedString `xml:"Name"`
HRShortName TranslatedString `xml:"ShortName"`
IssuerName string `xml:"IssuerID"`
SchemeManagerName string `xml:"SchemeManager"`
Name string `xml:"CredentialID"`
IsSingleton bool `xml:"ShouldBeSingleton"`
Description TranslatedString
Attributes []AttributeDescription `xml:"Attributes>Attribute"`
XMLVersion int `xml:"version,attr"`
XMLName xml.Name `xml:"IssueSpecification"`
}
// AttributeDescription is a description of an attribute within a credential type.
type AttributeDescription struct {
ID string `xml:"id,attr"`
Name TranslatedString
Description TranslatedString
}
// TranslatedString represents an XML tag containing a string translated to multiple languages.
// For example: <Foo id="bla"><Translation lang="en">Hello world</Translation><Translation lang="nl">Hallo wereld</Translation></Foo>
// type TranslatedString struct {
// Translations []struct {
// Language string `xml:"lang,attr"`
// Value string `xml:",chardata"`
// } `xml:"Translation"`
// ID string `xml:"id,attr"`
// }
//
// // Get returns the specified translation
// func (ts TranslatedString) Get(lang string) string {
// for _, l := range ts.Translations {
// if l.Language == lang {
// return l.Value
// }
// }
// return ""
// }
// TranslatedString represents an XML tag containing a string translated to multiple languages.
// For example: <Foo id="bla"><en>Hello world</en><nl>Hallo wereld</nl></Foo>
type TranslatedString struct {
Translations []struct {
XMLName xml.Name
Text string `xml:",chardata"`
} `xml:",any"`
}
// Translation returns the specified translation.
func (ts *TranslatedString) Translation(lang string) string {
for _, translation := range ts.Translations {
if translation.XMLName.Local == lang {
return translation.Text
}
}
return ""
}
// Identifier returns the identifier of the specified credential type.
func (cd *CredentialType) Identifier() string {
return cd.SchemeManagerName + "." + cd.IssuerName + "." + cd.Name
}
// IssuerIdentifier returns the issuer identifier of the specified credential type.
func (cd *CredentialType) IssuerIdentifier() string {
return cd.SchemeManagerName + "." + cd.IssuerName
}
// Identifier returns the identifier of the specified issuer description.
func (id *Issuer) Identifier() string {
return id.SchemeManagerName + "." + id.Name
}
// CurrentPublicKey returns the latest known public key of the issuer identified by this instance.
func (id *Issuer) CurrentPublicKey() *gabi.PublicKey {
keys := MetaStore.PublicKeys[id.Identifier()]
if keys == nil || len(keys) == 0 {
return nil
}
return keys[len(keys)-1]
}
// PublicKey returns the specified public key of the issuer identified by this instance.
func (id *Issuer) PublicKey(index int) *gabi.PublicKey {
keys := MetaStore.PublicKeys[id.Identifier()]
if keys == nil || index >= len(keys) {
return nil
}
return keys[index]
}
package irmago
import "testing"
import (
"fmt"
"math/big"
"os"
"testing"
"time"
"fmt"
"github.com/mhe/gabi"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
if len(gabi.MetaStore.SchemeManagers) == 0 { // FIXME
gabi.MetaStore.ParseFolder("testdata/irma_configuration")
if len(MetaStore.SchemeManagers) == 0 { // FIXME
MetaStore.ParseFolder("testdata/irma_configuration")
}
Manager = newCredentialManager()
err := os.RemoveAll("testdata/storage/test")
......@@ -36,6 +37,13 @@ func TestMain(m *testing.M) {
os.Exit(retCode)
}
// A convenience function for initializing big integers from known correct (10
// base) strings. Use with care, errors are ignored.
func s2big(s string) (r *big.Int) {
r, _ = new(big.Int).SetString(s, 10)
return
}
func parseAndroidStorage(t *testing.T) {
err := Manager.Init("testdata/storage/test")
assert.NoError(t, err, "Manager.Init() failed")
......@@ -68,3 +76,73 @@ func TestUnmarshaling(t *testing.T) {
verifyStoreIsUnmarshaled(t)
}
func TestParseStore(t *testing.T) {
err := MetaStore.ParseFolder("testdata/irma_configuration")
if err != nil {
t.Fatal(err)
}
assert.NotNil(t, MetaStore.Issuers["irma-demo.RU"].CurrentPublicKey().N, "irma-demo.RU public key has no modulus")
assert.Equal(t,
"Irma Demo",
MetaStore.SchemeManagers["irma-demo"].HRName.Translation("en"),
"irma-demo scheme manager has unexpected name")
assert.Equal(t,
"Radboud Universiteit Nijmegen",
MetaStore.Issuers["irma-demo.RU"].HRName.Translation("en"),
"irma-demo.RU issuer has unexpected name")
assert.Equal(t,
"Student Card",
MetaStore.Credentials["irma-demo.RU.studentCard"].HRShortName.Translation("en"),
"irma-demo.RU.studentCard has unexpected name")
assert.Equal(t,
"studentID",
MetaStore.Credentials["irma-demo.RU.studentCard"].Attributes[2].ID,
"irma-demo.RU.studentCard.studentID has unexpected name")
// Hash algorithm pseudocode:
// Base64(SHA256("irma-demo.RU.studentCard")[0:16])
assert.Contains(t, MetaStore.reverseHashes, "1stqlPad5edpfS1Na1U+DA==",
"irma-demo.RU.studentCard had improper hash")
assert.Contains(t, MetaStore.reverseHashes, "CLjnADMBYlFcuGOT7Z0xRg==",
"irma-demo.MijnOverheid.root had improper hash")
}
func TestMetadataAttribute(t *testing.T) {
metadata := NewMetadataAttribute()
if metadata.Version() != 0x02 {
t.Errorf("Unexpected metadata version: %d", metadata.Version())
}
expiry := metadata.SigningDate().Unix() + int64(metadata.ValidityDuration()*ExpiryFactor)
if !time.Unix(expiry, 0).Equal(metadata.Expiry()) {
t.Errorf("Invalid signing date")
}
if metadata.KeyCounter() != 0 {
t.Errorf("Unexpected key counter")
}
}
func TestMetadataCompatibility(t *testing.T) {
err := MetaStore.ParseFolder("testdata/irma_configuration")
if err != nil {
t.Fatal(err)
}
// An actual metadata attribute of an IRMA credential extracted from the IRMA app
attr := MetadataFromInt(s2big("49043481832371145193140299771658227036446546573739245068"))
assert.NotNil(t, attr.CredentialType(), "attr.CredentialType() should not be nil")
assert.Equal(t,
"irma-demo.RU.studentCard",
attr.CredentialType().Identifier(),
"Metadata credential type was not irma-demo.RU.studentCard",
)
assert.Equal(t, byte(0x02), attr.Version(), "Unexpected metadata version")
assert.Equal(t, time.Unix(1499904000, 0), attr.SigningDate(), "Unexpected signing date")
assert.Equal(t, time.Unix(1516233600, 0), attr.Expiry(), "Unexpected expiry date")
assert.Equal(t, 2, attr.KeyCounter(), "Unexpected key counter")
}
......@@ -19,12 +19,12 @@ type CredentialManager struct {
secretkey *big.Int
storagePath string
attributes map[string][]*AttributeList
credentials map[string]map[int]*gabi.Credential
credentials map[string]map[int]*Credential
}
func newCredentialManager() *CredentialManager {
return &CredentialManager{
credentials: make(map[string]map[int]*gabi.Credential),
credentials: make(map[string]map[int]*Credential),
}
}
......@@ -60,10 +60,10 @@ func (cm *CredentialManager) attrs(id string) []*AttributeList {
}
// creds returns cm.credentials[id], initializing it to an empty map if neccesary
func (cm *CredentialManager) creds(id string) map[int]*gabi.Credential {
func (cm *CredentialManager) creds(id string) map[int]*Credential {
list, exists := cm.credentials[id]
if !exists {
list = make(map[int]*gabi.Credential)
list = make(map[int]*Credential)
cm.credentials[id] = list
}
return list
......@@ -79,7 +79,7 @@ func (cm *CredentialManager) Attributes(id string, counter int) (attributes *Att
}
// Credential returns the requested credential, or nil if we do not have it.
func (cm *CredentialManager) Credential(id string, counter int) (cred *gabi.Credential, err error) {
func (cm *CredentialManager) Credential(id string, counter int) (cred *Credential, err error) {
// If the requested credential is not in credential map, we check if its attributes were
// deserialized during Init(). If so, there should be a corresponding signature file,
// so we read that, construct the credential, and add it to the credential map
......@@ -97,7 +97,11 @@ func (cm *CredentialManager) Credential(id string, counter int) (cred *gabi.Cred
err = errors.New("signature file not found")
return nil, err
}
cred := gabi.NewCredential(ints, sig, nil)
cred := newCredential(&gabi.Credential{
Attributes: ints,
Signature: sig,
Pk: nil, // TODO
})
cm.credentials[id][counter] = cred
}
......@@ -135,9 +139,8 @@ func (cm *CredentialManager) ParseAndroidStorage() (err error) {
for _, list := range parsedjson {
cm.secretkey = list[0].Attributes[0]
for i, cred := range list {
// TODO move this metadata initialisation somehow into gabi.Credential?
cred.MetadataAttribute = gabi.MetadataFromInt(cred.Attributes[1])
for i, gabicred := range list {
cred := newCredential(gabicred)
if cred.CredentialType() == nil {
return errors.New("cannot add unknown credential type")
}
......@@ -164,19 +167,19 @@ func (cm *CredentialManager) ParseAndroidStorage() (err error) {
return
}
func (cm *CredentialManager) addCredential(cred *gabi.Credential) {
func (cm *CredentialManager) addCredential(cred *Credential) {
id := cred.CredentialType().Identifier()
cm.attributes[id] = append(cm.attrs(id), NewAttributeListFromInts(cred.Attributes[1:]))
if _, exists := cm.credentials[id]; !exists {
cm.credentials[id] = make(map[int]*gabi.Credential)
cm.credentials[id] = make(map[int]*Credential)
}
counter := len(cm.attributes[id]) - 1
cm.credentials[id][counter] = cred
}
// Add adds the specified credential to the CredentialManager.
func (cm *CredentialManager) Add(cred *gabi.Credential) (err error) {
func (cm *CredentialManager) Add(cred *Credential) (err error) {
if cred.CredentialType() == nil {
return errors.New("cannot add unknown credential type")
}
......
......@@ -9,9 +9,10 @@ import (
"crypto/rand"
"encoding/hex"
"github.com/mhe/gabi"
"math/big"
"path"
"github.com/mhe/gabi"
)
// Filenames in which we store stuff
......@@ -94,7 +95,7 @@ func (cm *CredentialManager) saveFile(filepath string, content []byte) (err erro
return os.Rename(dir+"/"+tempfilename, filepath)
}
func (cm *CredentialManager) storeSignature(cred *gabi.Credential, counter int) (err error) {
func (cm *CredentialManager) storeSignature(cred *Credential, counter int) (err error) {
if cred.CredentialType() == nil {
return errors.New("cannot add unknown credential type")
}
......
package irmago
import (
"crypto/sha256"
"encoding/base64"
"encoding/xml"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/mhe/gabi"
)
// MetaStore is the global instance of ConfigurationStore
var MetaStore = newConfigurationStore()
// ConfigurationStore keeps track of scheme managers, issuers, credential types and public keys.
// Use the global MetaStore instance.
type ConfigurationStore struct {
SchemeManagers map[string]*SchemeManager
Issuers map[string]*Issuer
Credentials map[string]*CredentialType
PublicKeys map[string][]*gabi.PublicKey
reverseHashes map[string]string
}
func newConfigurationStore() (store *ConfigurationStore) {
store = &ConfigurationStore{
make(map[string]*SchemeManager),
make(map[string]*Issuer),
make(map[string]*CredentialType),
make(map[string][]*gabi.PublicKey),
make(map[string]string),
}
return
}
// PublicKey returns the specified public key, or nil if not present in the ConfigurationStore.
func (store *ConfigurationStore) PublicKey(id string, counter int) *gabi.PublicKey {
if list, ok := MetaStore.PublicKeys[id]; ok {
if len(list) > counter {
return list[counter]
}
}
return nil
}
func (store *ConfigurationStore) addReverseHash(credid string) {
hash := sha256.Sum256([]byte(credid))
store.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
}
func (store *ConfigurationStore) hashToCredentialType(hash []byte) *CredentialType {
if str, exists := store.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
return store.Credentials[str]
}
return nil
}
// ParseFolder populates the current store by parsing the specified irma_configuration folder,
// listing the containing scheme managers, issuers, credential types and public keys.
func (store *ConfigurationStore) ParseFolder(path string) error {
return iterateSubfolders(path, func(dir string) error {
manager := &SchemeManager{}
exists, err := pathToDescription(dir+"/description.xml", manager)
if err != nil {
return err