...
 
Commits (12)
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.0-rc.1] - 2020-03-03
### Added
- Include `clientReturnURL` in session request
### Changed
- All (translated) names of issuers and credential types of demo schemes (i.e. `irma-demo`) must now start with `Demo `
- `irmaclient` now uses bbolt for storage
- When the `irmaclient` receives a credential identical to another older one, the older one is overwritten
- Scheme signing and verification now supports symlinks
### Fixed
- Unclear error message when the request's `Content-Type` HTTP header is not properly set
- Unclear error message when non-optional attributes are missing in issuance request
- Scheme verification now ignores deprecated issuers and keys and ignores missing IssueURL tags in credential types
- `irma server` no longer crashes at startup if no network interfaces are available
- Various bugs in `irma server` configuration
## [0.4.1] - 2019-10-15
### Changed
- Renamed `irma session` flag `--authmethod` to `--auth-method` for consistency with server `Configuration` struct
### Fixed
- Fix bug that would prevent downloading of demo private keys of demo schemes on server startup and scheme updating
- `irma server` now respects the `disable_schemes_update` option like the `irmaserver` library (#63)
- Other small fixes
## [0.4.0] - 2019-10-09
### Added
- New irma server feature: static (e.g. printable) QRs that start preconfigured sessions, see [documentation](https://irma.app/docs/irma-server/#static-irma-qrs)
- irma server now returns attribute issuance time to the requestor after the session has finished
### Fixed
- Hopefully fix “unknown or expired session” errors that would sometimes occur in the IRMA app in bad network conditions
- Combined issuance-disclosure requests with two schemes one of which has a keyshare server now work as expected
- Various other bugfixes
[0.5.0-rc.1]: https://github.com/privacybydesign/irmago/compare/v0.4.1...v0.5.0-rc.1
[0.4.1]: https://github.com/privacybydesign/irmago/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/privacybydesign/irmago/tree/v0.4.0
......@@ -30,7 +30,7 @@ func TestKeyshareRegister(t *testing.T) {
defer test.ClearTestStorage(t)
require.NoError(t, client.KeyshareRemoveAll())
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
client.KeyshareEnroll(irma.NewSchemeManagerIdentifier("test"), nil, "12345", "en")
require.NoError(t, <-handler.c)
......
......@@ -120,7 +120,7 @@ func TestLargeAttribute(t *testing.T) {
client, _ := parseStorage(t)
defer test.ClearTestStorage(t)
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
issuanceRequest := getSpecialIssuanceRequest(false, "1234567890123456789012345678901234567890") // 40 chars
sessionHelper(t, issuanceRequest, "issue", client)
......@@ -204,7 +204,7 @@ that they have been fixed. */
func TestAttributeByteEncoding(t *testing.T) {
client, _ := parseStorage(t)
defer test.ClearTestStorage(t)
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
/* After bitshifting the presence bit into the large attribute below, the most significant
bit is 1. In the bigint->[]byte conversion that happens before hashing this attribute, in
......@@ -229,7 +229,7 @@ func TestOutdatedClientIrmaConfiguration(t *testing.T) {
defer test.ClearTestStorage(t)
// Remove old studentCard credential from before support for optional attributes, and issue a new one
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
require.Nil(t, requestorSessionHelper(t, getIssuanceRequest(true), client).Err)
// client does not have updated irma_configuration with new attribute irma-demo.RU.studentCard.newAttribute,
......@@ -249,7 +249,7 @@ func TestDisclosureNewAttributeUpdateSchemeManager(t *testing.T) {
require.False(t, client.Configuration.CredentialTypes[credid].ContainsAttribute(attrid))
// Remove old studentCard credential from before support for optional attributes, and issue a new one
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
require.Nil(t, requestorSessionHelper(t, getIssuanceRequest(true), client).Err)
// Trigger downloading the updated irma_configuration using a disclosure request containing the
......
......@@ -331,33 +331,34 @@ func (client *Client) RemoveCredentialByHash(hash string) error {
return client.RemoveCredential(cred.CredentialType().Identifier(), index)
}
// RemoveAllCredentials removes all credentials.
func (client *Client) RemoveAllCredentials() error {
removed := map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
for _, attrlistlist := range client.attributes {
for _, attrs := range attrlistlist {
if attrs.CredentialType() != nil {
removed[attrs.CredentialType().Identifier()] = attrs.Strings()
}
}
// Removes all attributes, signatures, logs and userdata
// Includes the user's secret key, keyshare servers and preferences/updates
// A fresh secret key is installed.
func (client *Client) RemoveStorage() error {
var err error
// Remove data from memory
client.attributes = make(map[irma.CredentialTypeIdentifier][]*irma.AttributeList)
client.keyshareServers = make(map[irma.SchemeManagerIdentifier]*keyshareServer)
client.credentialsCache = make(map[irma.CredentialTypeIdentifier]map[int]*credential)
if err = client.storage.DeleteAll(); err != nil {
return err
}
// Client assumes there is always a secret key, so we have to load a new one
client.secretkey, err = client.storage.LoadSecretKey()
if err != nil {
return err
}
client.attributes = map[irma.CredentialTypeIdentifier][]*irma.AttributeList{}
logentry := &LogEntry{
Type: ActionRemoval,
Time: irma.Timestamp(time.Now()),
Removed: removed,
// TODO: do we consider this setting as user data?
if client.Preferences, err = client.storage.LoadPreferences(); err != nil {
return err
}
client.applyPreferences()
return client.storage.Transaction(func(tx *transaction) error {
if err := client.storage.TxDeleteAllAttributes(tx); err != nil {
return err
}
if err := client.storage.TxDeleteAllSignatures(tx); err != nil {
return err
}
return client.storage.TxAddLogEntry(tx, logentry)
})
return nil
}
// Attribute and credential getter methods
......
......@@ -3,6 +3,7 @@ package irmaclient
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"testing"
......@@ -362,6 +363,31 @@ func TestUpdatingStorage(t *testing.T) {
}
}
func TestRemoveStorage(t *testing.T) {
client := parseStorage(t)
defer test.ClearTestStorage(t)
bucketsBefore := map[string]bool{"attrs": true, "sigs": true, "userdata": true, "logs": true} // Test storage has 1 log
bucketsAfter := map[string]bool{"attrs": false, "sigs": false, "userdata": true, "logs": false} // Userdata should hold a new secret key
old_sk := *client.secretkey
// Check that buckets exist
for name, exists := range bucketsBefore {
require.Equal(t, exists, client.storage.BucketExists([]byte(name)), fmt.Sprintf("Bucket \"%s\" exists should be %t", name, exists))
}
require.NoError(t, client.RemoveStorage())
for name, exists := range bucketsAfter {
require.Equal(t, exists, client.storage.BucketExists([]byte(name)), fmt.Sprintf("Bucket \"%s\" exists should be %t", name, exists))
}
// Check that the client has a new secret key
new_sk := *client.secretkey
require.NotEqual(t, old_sk, new_sk)
}
// ------
type TestClientHandler struct {
......
......@@ -65,6 +65,15 @@ func (s *storage) Close() error {
return s.db.Close()
}
func (s *storage) BucketExists(name []byte) bool {
return s.db.View(func(tx *bbolt.Tx) error {
if tx.Bucket(name) == nil {
return bbolt.ErrBucketNotFound
}
return nil
}) == nil
}
func (s *storage) txStore(tx *transaction, bucketName string, key string, value interface{}) error {
b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
......@@ -339,3 +348,33 @@ func (s *storage) LoadPreferences() (Preferences, error) {
_, err := s.load(userdataBucket, preferencesKey, &config)
return config, err
}
func (s *storage) TxDeleteUserdata(tx *transaction) error {
return tx.DeleteBucket([]byte(userdataBucket))
}
func (s *storage) TxDeleteLogs(tx *transaction) error {
return tx.DeleteBucket([]byte(logsBucket))
}
func (s *storage) TxDeleteAll(tx *transaction) error {
if err := s.TxDeleteAllAttributes(tx); err != nil && err != bbolt.ErrBucketNotFound {
return err
}
if err := s.TxDeleteAllSignatures(tx); err != nil && err != bbolt.ErrBucketNotFound {
return err
}
if err := s.TxDeleteUserdata(tx); err != nil && err != bbolt.ErrBucketNotFound {
return err
}
if err := s.TxDeleteLogs(tx); err != nil && err != bbolt.ErrBucketNotFound {
return err
}
return nil
}
func (s *storage) DeleteAll() error {
return s.Transaction(func(tx *transaction) error {
return s.TxDeleteAll(tx)
})
}
......@@ -104,6 +104,10 @@ const (
privkeyPattern = "%s/%s/%s/PrivateKeys/*.xml"
)
var (
validLangs = []string{"en", "nl"} // Hardcode these for now, TODO make configurable
)
func (sme SchemeManagerError) Error() string {
return fmt.Sprintf("Error parsing scheme manager %s: %s", sme.Manager.Name(), sme.Err.Error())
}
......@@ -1313,6 +1317,15 @@ func (conf *Configuration) StopAutoUpdateSchemes() {
}
// Validation methods containing consistency checks on irma_configuration
func validateDemoPrefix(ts TranslatedString) error {
prefix := "Demo "
for _, lang := range validLangs {
if !strings.HasPrefix(map[string]string(ts)[lang], prefix) {
return errors.Errorf("value in language %s is not prefixed with '%s'", lang, prefix)
}
}
return nil
}
func (conf *Configuration) validateIssuer(manager *SchemeManager, issuer *Issuer, dir string) error {
issuerid := issuer.Identifier()
......@@ -1333,6 +1346,9 @@ func (conf *Configuration) validateIssuer(manager *SchemeManager, issuer *Issuer
if manager.ID != issuer.SchemeManagerID {
return errors.Errorf("Issuer %s has wrong SchemeManager %s", issuerid.String(), issuer.SchemeManagerID)
}
if err = validateDemoPrefix(issuer.Name); manager.Demo && err != nil {
return errors.Errorf("Name of demo issuer %s invalid: %s", issuer.ID, err.Error())
}
if err = fs.AssertPathExists(filepath.Join(dir, "logo.png")); err != nil {
conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no logo.png", issuerid.String()))
}
......@@ -1354,6 +1370,9 @@ func (conf *Configuration) validateCredentialType(manager *SchemeManager, issuer
if cred.SchemeManagerID != manager.ID {
return errors.Errorf("Credential type %s has wrong SchemeManager %s", credid.String(), cred.SchemeManagerID)
}
if err := validateDemoPrefix(cred.Name); manager.Demo && err != nil {
return errors.Errorf("Name of demo credential %s invalid: %s", cred.ID, err.Error())
}
if err := fs.AssertPathExists(filepath.Join(dir, "logo.png")); err != nil {
conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has no logo.png", credid.String()))
}
......@@ -1406,7 +1425,6 @@ func (conf *Configuration) validateScheme(scheme *SchemeManager, dir string) err
// validateTranslations checks for each member of the interface o that is of type TranslatedString
// that it contains all necessary translations.
func (conf *Configuration) validateTranslations(file string, o interface{}) {
langs := []string{"en", "nl"} // Hardcode these for now, TODO make configurable
v := reflect.ValueOf(o)
// Dereference in case of pointer or interface
......@@ -1421,7 +1439,7 @@ func (conf *Configuration) validateTranslations(file string, o interface{}) {
continue
}
val := field.Interface().(TranslatedString)
for _, lang := range langs {
for _, lang := range validLangs {
if _, exists := val[lang]; !exists {
conf.Warnings = append(conf.Warnings, fmt.Sprintf("%s misses %s translation in <%s> tag", file, lang, name))
}
......
......@@ -312,9 +312,14 @@ func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
return
}
if !applies {
s.conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s",
server.ToJson(r.Header), string(body))
server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authorized")
var ctype = r.Header.Get("Content-Type")
if ctype != "application/json" && ctype != "text/plain" {
s.conf.Logger.Warnf("Session request uses unsupported Content-Type: %s", ctype)
server.WriteError(w, server.ErrorInvalidRequest, "Unsupported Content-Type: "+ctype)
return
}
s.conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s", server.ToJson(r.Header), string(body))
server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authenticated")
return
}
......
......@@ -7,7 +7,7 @@ package irma
import "github.com/timshannon/bolthold"
// Version of the IRMA command line and libraries
const Version = "0.4.1"
const Version = "0.5.0-rc.1"
// go-atum requires a version of bolthold newer than the latest release v1.1, but go-atum does not
// use dep, so by default dep fetches v1.1 which breaks the build. We make bolthold an explicit
......