Commit 3c2a5508 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Add scheme manager authenticity through ECDSA signatures

Authenticity of the scheme manager files in the irma_configuration
folder is now checked as follows.
Each scheme manager must henceforth include three new files in its
root folder: index, index.sig, and pk.pem. index lists all files
under this manager as well as their SHA256-hash. index.sig is a
ECDSA signature over index, signed with the private key corresponding
to the public key pk.pem. Thus, a file under the scheme manager
is authenticated if its hash matches the one in the index file,
and if the signature over the index file is valid.

Authenticity of all xml files, including the Idemix public keys,
is verified in this way automatically when these files are loaded.
For issuer and credential logos, use Configuration.ReadAuthenticatedFile.
parent 0b89b56f
...@@ -21,6 +21,8 @@ type SchemeManager struct { ...@@ -21,6 +21,8 @@ type SchemeManager struct {
KeyshareAttribute string KeyshareAttribute string
XMLVersion int `xml:"version,attr"` XMLVersion int `xml:"version,attr"`
XMLName xml.Name `xml:"SchemeManager"` XMLName xml.Name `xml:"SchemeManager"`
index SchemeManagerIndex
} }
// Issuer describes an issuer. // Issuer describes an issuer.
......
...@@ -11,14 +11,16 @@ import ( ...@@ -11,14 +11,16 @@ import (
) )
// AssertPathExists returns nil only if it has been successfully // AssertPathExists returns nil only if it has been successfully
// verified that the specified path exists. // verified that all specified paths exists.
func AssertPathExists(path string) error { func AssertPathExists(paths ...string) error {
exist, err := PathExists(path) for _, p := range paths {
if err != nil { exist, err := PathExists(p)
return err if err != nil {
} return err
if !exist { }
return errors.Errorf("Path %s does not exist", path) if !exist {
return errors.Errorf("Path %s does not exist", p)
}
} }
return nil return nil
} }
......
...@@ -14,6 +14,18 @@ import ( ...@@ -14,6 +14,18 @@ import (
"strings" "strings"
"sort"
"bytes"
"encoding/hex"
"crypto/ecdsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"math/big"
"github.com/credentials/irmago/internal/fs" "github.com/credentials/irmago/internal/fs"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/mhe/gabi" "github.com/mhe/gabi"
...@@ -32,6 +44,10 @@ type Configuration struct { ...@@ -32,6 +44,10 @@ type Configuration struct {
initialized bool initialized bool
} }
type ConfigurationFileHash []byte
type SchemeManagerIndex map[string]ConfigurationFileHash
// NewConfiguration returns a new configuration. After this // NewConfiguration returns a new configuration. After this
// ParseFolder() should be called to parse the specified path. // ParseFolder() should be called to parse the specified path.
func NewConfiguration(path string, assets string) (conf *Configuration, err error) { func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
...@@ -64,18 +80,25 @@ func (conf *Configuration) ParseFolder() error { ...@@ -64,18 +80,25 @@ func (conf *Configuration) ParseFolder() error {
err := iterateSubfolders(conf.path, func(dir string) error { err := iterateSubfolders(conf.path, func(dir string) error {
manager := &SchemeManager{} manager := &SchemeManager{}
exists, err := pathToDescription(dir+"/description.xml", manager) if err := conf.ParseIndex(manager, dir); err != nil {
if err != nil {
return err return err
} }
if !exists { exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
return nil if err != nil || !exists {
return err
} }
if manager.XMLVersion < 7 { if manager.XMLVersion < 7 {
return errors.New("Unsupported scheme manager description") return errors.New("Unsupported scheme manager description")
} }
valid, err := conf.VerifySignature(manager.Identifier())
if err != nil {
return err
}
if !valid {
return errors.New("Scheme manager signature was invalid")
}
conf.SchemeManagers[manager.Identifier()] = manager conf.SchemeManagers[manager.Identifier()] = manager
return conf.parseIssuerFolders(dir) return conf.parseIssuerFolders(manager, dir)
}) })
if err != nil { if err != nil {
return err return err
...@@ -84,11 +107,15 @@ func (conf *Configuration) ParseFolder() error { ...@@ -84,11 +107,15 @@ func (conf *Configuration) ParseFolder() error {
return nil return nil
} }
func relativePath(absolute string, relative string) string {
return relative[len(absolute)+1:]
}
// PublicKey returns the specified public key, or nil if not present in the Configuration. // PublicKey returns the specified public key, or nil if not present in the Configuration.
func (conf *Configuration) PublicKey(id IssuerIdentifier, counter int) (*gabi.PublicKey, error) { func (conf *Configuration) PublicKey(id IssuerIdentifier, counter int) (*gabi.PublicKey, error) {
if _, contains := conf.publicKeys[id]; !contains { if _, contains := conf.publicKeys[id]; !contains {
conf.publicKeys[id] = map[int]*gabi.PublicKey{} conf.publicKeys[id] = map[int]*gabi.PublicKey{}
if err := conf.parseKeysFolder(id); err != nil { if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
return nil, err return nil, err
} }
} }
...@@ -112,10 +139,10 @@ func (conf *Configuration) IsInitialized() bool { ...@@ -112,10 +139,10 @@ func (conf *Configuration) IsInitialized() bool {
return conf.initialized return conf.initialized
} }
func (conf *Configuration) parseIssuerFolders(path string) error { func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
return iterateSubfolders(path, func(dir string) error { return iterateSubfolders(path, func(dir string) error {
issuer := &Issuer{} issuer := &Issuer{}
exists, err := pathToDescription(dir+"/description.xml", issuer) exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
if err != nil { if err != nil {
return err return err
} }
...@@ -126,12 +153,12 @@ func (conf *Configuration) parseIssuerFolders(path string) error { ...@@ -126,12 +153,12 @@ func (conf *Configuration) parseIssuerFolders(path string) error {
return errors.New("Unsupported issuer description") return errors.New("Unsupported issuer description")
} }
conf.Issuers[issuer.Identifier()] = issuer conf.Issuers[issuer.Identifier()] = issuer
return conf.parseCredentialsFolder(dir + "/Issues/") return conf.parseCredentialsFolder(manager, dir+"/Issues/")
}) })
} }
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ... // parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error { func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name()) path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
files, err := filepath.Glob(path) files, err := filepath.Glob(path)
if err != nil { if err != nil {
...@@ -145,7 +172,11 @@ func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error { ...@@ -145,7 +172,11 @@ func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
if err != nil { if err != nil {
continue continue
} }
pk, err := gabi.NewPublicKeyFromFile(file) bts, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.path, file))
if err != nil {
return err
}
pk, err := gabi.NewPublicKeyFromBytes(bts)
if err != nil { if err != nil {
return err return err
} }
...@@ -157,10 +188,10 @@ func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error { ...@@ -157,10 +188,10 @@ func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
} }
// parse $schememanager/$issuer/Issues/*/description.xml // parse $schememanager/$issuer/Issues/*/description.xml
func (conf *Configuration) parseCredentialsFolder(path string) error { func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, path string) error {
return iterateSubfolders(path, func(dir string) error { return iterateSubfolders(path, func(dir string) error {
cred := &CredentialType{} cred := &CredentialType{}
exists, err := pathToDescription(dir+"/description.xml", cred) exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
if err != nil { if err != nil {
return err return err
} }
...@@ -203,23 +234,17 @@ func iterateSubfolders(path string, handler func(string) error) error { ...@@ -203,23 +234,17 @@ func iterateSubfolders(path string, handler func(string) error) error {
return nil return nil
} }
func pathToDescription(path string, description interface{}) (bool, error) { func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
if _, err := os.Stat(path); err != nil { if _, err := os.Stat(path); err != nil {
return false, nil return false, nil
} }
file, err := os.Open(path) bts, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.path, path))
if err != nil {
return true, err
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil { if err != nil {
return true, err return true, err
} }
err = xml.Unmarshal(bytes, description) err = xml.Unmarshal(bts, description)
if err != nil { if err != nil {
return true, err return true, err
} }
...@@ -329,17 +354,36 @@ func (conf *Configuration) AddSchemeManager(manager *SchemeManager) error { ...@@ -329,17 +354,36 @@ func (conf *Configuration) AddSchemeManager(manager *SchemeManager) error {
if err := fs.EnsureDirectoryExists(fmt.Sprintf("%s/%s", conf.path, name)); err != nil { if err := fs.EnsureDirectoryExists(fmt.Sprintf("%s/%s", conf.path, name)); err != nil {
return err return err
} }
b, err := xml.Marshal(manager)
if err != nil { t := NewHTTPTransport(manager.URL)
path := fmt.Sprintf("%s/%s", conf.path, name)
if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
return err
}
if err := t.GetFile("/pk.pem", path+"/pk.pem"); err != nil {
return err return err
} }
if err := fs.SaveFile(fmt.Sprintf("%s/%s/description.xml", conf.path, name), b); err != nil { if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
return err return err
} }
conf.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager conf.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager
return nil return nil
} }
func (conf *Configuration) DownloadSchemeManagerSignature(manager *SchemeManager) error {
t := NewHTTPTransport(manager.URL)
path := fmt.Sprintf("%s/%s", conf.path, manager.ID)
if err := t.GetFile("/index", path+"/index"); err != nil {
return err
}
if err := t.GetFile("/index.sig", path+"/index.sig"); err != nil {
return err
}
return nil
}
func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, error) { func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, error) {
var contains bool var contains bool
var err error var err error
...@@ -348,6 +392,7 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, ...@@ -348,6 +392,7 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet,
Issuers: map[IssuerIdentifier]struct{}{}, Issuers: map[IssuerIdentifier]struct{}{},
CredentialTypes: map[CredentialTypeIdentifier]struct{}{}, CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
} }
updatedManagers := make(map[SchemeManagerIdentifier]struct{})
for manid := range set.SchemeManagers { for manid := range set.SchemeManagers {
if _, contains = conf.SchemeManagers[manid]; !contains { if _, contains = conf.SchemeManagers[manid]; !contains {
...@@ -358,14 +403,16 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, ...@@ -358,14 +403,16 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet,
transport := NewHTTPTransport("") transport := NewHTTPTransport("")
for issid := range set.Issuers { for issid := range set.Issuers {
if _, contains = conf.Issuers[issid]; !contains { if _, contains = conf.Issuers[issid]; !contains {
url := conf.SchemeManagers[issid.SchemeManagerIdentifier()].URL + "/" + issid.Name() manager := issid.SchemeManagerIdentifier()
path := fmt.Sprintf("%s/%s/%s", conf.path, issid.SchemeManagerIdentifier().String(), issid.Name()) url := conf.SchemeManagers[manager].URL + "/" + issid.Name()
path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), issid.Name())
if err = transport.GetFile(url+"/description.xml", path+"/description.xml"); err != nil { if err = transport.GetFile(url+"/description.xml", path+"/description.xml"); err != nil {
return nil, err return nil, err
} }
if err = transport.GetFile(url+"/logo.png", path+"/logo.png"); err != nil { if err = transport.GetFile(url+"/logo.png", path+"/logo.png"); err != nil {
return nil, err return nil, err
} }
updatedManagers[manager] = struct{}{}
downloaded.Issuers[issid] = struct{}{} downloaded.Issuers[issid] = struct{}{}
} }
} }
...@@ -382,6 +429,7 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, ...@@ -382,6 +429,7 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet,
if err = transport.GetFile(conf.SchemeManagers[manager].URL+suffix, path); err != nil { if err = transport.GetFile(conf.SchemeManagers[manager].URL+suffix, path); err != nil {
return nil, err return nil, err
} }
updatedManagers[manager] = struct{}{}
} }
} }
} }
...@@ -403,9 +451,132 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, ...@@ -403,9 +451,132 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet,
fmt.Sprintf("%s/%s/Issues/%s/logo.png", conf.SchemeManagers[manager].URL, issuer.Name(), credid.Name()), fmt.Sprintf("%s/%s/Issues/%s/logo.png", conf.SchemeManagers[manager].URL, issuer.Name(), credid.Name()),
fmt.Sprintf("%s/%s/logo.png", local, credid.Name()), fmt.Sprintf("%s/%s/logo.png", local, credid.Name()),
) )
updatedManagers[manager] = struct{}{}
downloaded.CredentialTypes[credid] = struct{}{} downloaded.CredentialTypes[credid] = struct{}{}
} }
} }
return downloaded, conf.ParseFolder() for manager := range updatedManagers {
if err := conf.DownloadSchemeManagerSignature(conf.SchemeManagers[manager]); err != nil {
return nil, err
}
}
if !downloaded.Empty() {
return downloaded, conf.ParseFolder()
}
return downloaded, nil
}
func (i SchemeManagerIndex) String() string {
var paths []string
var b bytes.Buffer
for path := range i {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
b.WriteString(hex.EncodeToString(i[path]))
b.WriteString(" ")
b.WriteString(path)
b.WriteString("\n")
}
return b.String()
}
func (i SchemeManagerIndex) FromString(s string) error {
for j, line := range strings.Split(s, "\n") {
if len(line) == 0 {
continue
}
parts := strings.Split(line, " ")
if len(parts) != 2 {
return errors.Errorf("Scheme manager index line %d has incorrect amount of parts", j)
}
hash, err := hex.DecodeString(parts[0])
if err != nil {
return err
}
i[parts[1]] = hash
}
return nil
}
func (conf *Configuration) ParseIndex(manager *SchemeManager, dir string) error {
if err := fs.AssertPathExists(dir + "/index"); err != nil {
return errors.New("Missing scheme manager index file")
}
indexbts, err := ioutil.ReadFile(dir + "/index")
if err != nil {
return err
}
manager.index = make(map[string]ConfigurationFileHash)
return manager.index.FromString(string(indexbts))
}
// ReadAuthenticatedFile reads the file at the specified path
// and verifies its authenticity by checking that the file hash
// is present in the (signed) scheme manager index file.
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, error) {
signedHash, ok := manager.index[path]
if !ok {
return nil, errors.New("File not present in scheme manager index")
}
bts, err := ioutil.ReadFile(filepath.Join(conf.path, path))
if err != nil {
return nil, err
}
computedHash := sha256.Sum256(bts)
if !bytes.Equal(computedHash[:], signedHash) {
return nil, errors.New("File hash invalid")
}
return bts, nil
}
// VerifySignature verifies the signature on the scheme manager index file
// (which contains the SHA256 hashes of all files under this scheme manager,
// which are used for verifying file authenticity).
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (bool, error) {
dir := filepath.Join(conf.path, id.String())
if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
return false, errors.New("Missing scheme manager index file, signature, or public key")
}
// Read and hash index file
indexbts, err := ioutil.ReadFile(dir + "/index")
if err != nil {
return false, err
}
indexhash := sha256.Sum256(indexbts)
// Read and parse scheme manager public key
pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
if err != nil {
return false, err
}
pkblk, _ := pem.Decode(pkbts)
genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
if err != nil {
return false, err
}
pk, ok := genericPk.(*ecdsa.PublicKey)
if !ok {
return false, errors.New("Invalid scheme manager public key")
}
// Read and parse signature
sig, err := ioutil.ReadFile(dir + "/index.sig")
if err != nil {
return false, err
}
ints := make([]*big.Int, 0, 2)
_, err = asn1.Unmarshal(sig, &ints)
// Verify signature
return ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]), nil
} }
package main
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"strings"
"github.com/credentials/irmago"
"github.com/credentials/irmago/internal/fs"
)
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: irmasig path_to_private_key path_to_irma_configuration")
os.Exit(0)
}
// Validate arguments
privatekey, err := readPrivateKey(os.Args[1])
if err != nil {
die("Failed to read private key:", err)
}
confpath, err := filepath.Abs(os.Args[2])
if err != nil {
die("Invalid path", err)
}
if err = fs.AssertPathExists(confpath); err != nil {
die("Specified path does not exist", nil)
}
// Traverse dir and add file hashes to index
var index irma.SchemeManagerIndex = make(map[string]irma.ConfigurationFileHash)
err = filepath.Walk(confpath, func(path string, info os.FileInfo, err error) error {
return calculateFileHash(path, info, err, confpath, index)
})
if err != nil {
die("Failed to calculate file index:", err)
}
// Write index.xml
bts := []byte(index.String())
if err = ioutil.WriteFile(confpath+"/index", bts, 0644); err != nil {
die("Failed to write index.xml", err)
}
// Create and write signature
indexHash := sha256.Sum256(bts)
r, s, err := ecdsa.Sign(rand.Reader, privatekey, indexHash[:])
if err != nil {
die("Failed to sign index:", err)
}
sigbytes, err := asn1.Marshal([]*big.Int{r, s})
if err != nil {
die("Failed to serialize signature:", err)
}
if err = ioutil.WriteFile(confpath+"/index.sig", sigbytes, 0644); err != nil {
die("Failed to write index.xml.sig", err)
}
// Write public key
bts, err = x509.MarshalPKIXPublicKey(&privatekey.PublicKey)
if err != nil {
die("Failed to serialize public key", err)
}
pemEncodedPub := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: bts})
ioutil.WriteFile(confpath+"/pk.pem", pemEncodedPub, 0644)
}
func readPrivateKey(path string) (*ecdsa.PrivateKey, error) {
bts, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(bts)
return x509.ParseECPrivateKey(block.Bytes)
}
func calculateFileHash(path string, info os.FileInfo, err error, confpath string, index irma.SchemeManagerIndex) error {
if err != nil {
return err
}
if info.IsDir() || // Can only sign files
strings.HasSuffix(path, "index") || // Skip the index file itself
strings.Contains(path, "/.git/") || // No need to traverse .git dirs
strings.Contains(path, "/PrivateKeys/") || // Don't sign private keys
(!strings.HasSuffix(path, ".xml") && !strings.HasSuffix(path, ".png")) {
return nil
}
bts, err := ioutil.ReadFile(path)
if err != nil {
return err
}
relativePath, err := filepath.Rel(confpath, path)
if err != nil {
return err
}
relativePath = filepath.Join(filepath.Base(confpath), relativePath)
hash := sha256.Sum256(bts)
index[relativePath] = hash[:]
return nil
}
func die(message string, err error) {