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 {
KeyshareAttribute string
XMLVersion int `xml:"version,attr"`
XMLName xml.Name `xml:"SchemeManager"`
index SchemeManagerIndex
}
// Issuer describes an issuer.
......
......@@ -11,14 +11,16 @@ import (
)
// AssertPathExists returns nil only if it has been successfully
// verified that the specified path exists.
func AssertPathExists(path string) error {
exist, err := PathExists(path)
if err != nil {
return err
}
if !exist {
return errors.Errorf("Path %s does not exist", path)
// verified that all specified paths exists.
func AssertPathExists(paths ...string) error {
for _, p := range paths {
exist, err := PathExists(p)
if err != nil {
return err
}
if !exist {
return errors.Errorf("Path %s does not exist", p)
}
}
return nil
}
......
......@@ -14,6 +14,18 @@ import (
"strings"
"sort"
"bytes"
"encoding/hex"
"crypto/ecdsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"math/big"
"github.com/credentials/irmago/internal/fs"
"github.com/go-errors/errors"
"github.com/mhe/gabi"
......@@ -32,6 +44,10 @@ type Configuration struct {
initialized bool
}
type ConfigurationFileHash []byte
type SchemeManagerIndex map[string]ConfigurationFileHash
// NewConfiguration returns a new configuration. After this
// ParseFolder() should be called to parse the specified path.
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
......@@ -64,18 +80,25 @@ func (conf *Configuration) ParseFolder() error {
err := iterateSubfolders(conf.path, func(dir string) error {
manager := &SchemeManager{}
exists, err := pathToDescription(dir+"/description.xml", manager)
if err != nil {
if err := conf.ParseIndex(manager, dir); err != nil {
return err
}
if !exists {
return nil
exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
if err != nil || !exists {
return err
}
if manager.XMLVersion < 7 {
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
return conf.parseIssuerFolders(dir)
return conf.parseIssuerFolders(manager, dir)
})
if err != nil {
return err
......@@ -84,11 +107,15 @@ func (conf *Configuration) ParseFolder() error {
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.
func (conf *Configuration) PublicKey(id IssuerIdentifier, counter int) (*gabi.PublicKey, error) {
if _, contains := conf.publicKeys[id]; !contains {
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
}
}
......@@ -112,10 +139,10 @@ func (conf *Configuration) IsInitialized() bool {
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 {
issuer := &Issuer{}
exists, err := pathToDescription(dir+"/description.xml", issuer)
exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
if err != nil {
return err
}
......@@ -126,12 +153,12 @@ func (conf *Configuration) parseIssuerFolders(path string) error {
return errors.New("Unsupported issuer description")
}
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, ...
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())
files, err := filepath.Glob(path)
if err != nil {
......@@ -145,7 +172,11 @@ func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
if err != nil {
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 {
return err
}
......@@ -157,10 +188,10 @@ func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
}
// 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 {
cred := &CredentialType{}
exists, err := pathToDescription(dir+"/description.xml", cred)
exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
if err != nil {
return err
}
......@@ -203,23 +234,17 @@ func iterateSubfolders(path string, handler func(string) error) error {
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 {
return false, nil
}
file, err := os.Open(path)
if err != nil {
return true, err
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
bts, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.path, path))
if err != nil {
return true, err
}
err = xml.Unmarshal(bytes, description)
err = xml.Unmarshal(bts, description)
if err != nil {
return true, err
}
......@@ -329,17 +354,36 @@ func (conf *Configuration) AddSchemeManager(manager *SchemeManager) error {
if err := fs.EnsureDirectoryExists(fmt.Sprintf("%s/%s", conf.path, name)); err != nil {
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
}
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
}
conf.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager
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) {
var contains bool
var err error
......@@ -348,6 +392,7 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet,
Issuers: map[IssuerIdentifier]struct{}{},
CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
}
updatedManagers := make(map[SchemeManagerIdentifier]struct{})
for manid := range set.SchemeManagers {
if _, contains = conf.SchemeManagers[manid]; !contains {
......@@ -358,14 +403,16 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet,
transport := NewHTTPTransport("")
for issid := range set.Issuers {
if _, contains = conf.Issuers[issid]; !contains {
url := conf.SchemeManagers[issid.SchemeManagerIdentifier()].URL + "/" + issid.Name()
path := fmt.Sprintf("%s/%s/%s", conf.path, issid.SchemeManagerIdentifier().String(), issid.Name())
manager := issid.SchemeManagerIdentifier()
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 {
return nil, err
}
if err = transport.GetFile(url+"/logo.png", path+"/logo.png"); err != nil {
return nil, err
}
updatedManagers[manager] = struct{}{}
downloaded.Issuers[issid] = struct{}{}
}
}
......@@ -382,6 +429,7 @@ func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet,
if err = transport.GetFile(conf.SchemeManagers[manager].URL+suffix, path); err != nil {
return nil, err
}
updatedManagers[manager] = struct{}{}
}
}
}
......@@ -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/logo.png", local, credid.Name()),
)
updatedManagers[manager] = 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) {
if err != nil {
fmt.Println(message, err)
} else {
fmt.Println(message)
}
os.Exit(1)
}
5cef2de223075ea192407d66de274f8bc7104ab3537b455df14aca306b4809f7 irma-demo/MijnOverheid/Issues/root/description.xml
3571e30777cdf5b97acbb0820f1b69983d2dc2b6bae91c8fb67cfc79ef4e2543 irma-demo/MijnOverheid/PublicKeys/0.xml
115bd71b738cd2ba10e832559fc5d35ee8222f4049a0fc10fc18d0853c4aa4d9 irma-demo/MijnOverheid/PublicKeys/1.xml
3894294371b3c609278010364a2b5129a96ea26fe5a44c6625438f9877e79e84 irma-demo/MijnOverheid/description.xml
d81eeb49a992cbb9107cbec5304a8aaf9a932d1c9564510e76460036f69a083f irma-demo/MijnOverheid/logo.png
e71c1d8636e1097ff653d1473f1066d90ed8dee186b20a08501ba8d59d5cae4e irma-demo/RU/Issues/studentCard/description.xml
449a51cbb1ce540c88eaa54942d5200122859136de26b30fb02d23541a54f17b irma-demo/RU/PublicKeys/0.xml
ffa2349b0a638132837c767df12c7b15cbac85f648c614a0811b3edb751d0a6d irma-demo/RU/PublicKeys/1.xml
e298a2e6dca3bdb923d22734dc4f76ba7b48c5364eb8d7b60e6ec4e940921f89 irma-demo/RU/PublicKeys/2.xml
a4f6cc35cace3e9dc9388b29a8756ea83e5884f799d75cadd4efa60e1a12d855 irma-demo/RU/description.xml
35697bb7ffb19518a0ac6739ac3eef6b0272cd322c4619b075328b88c06ac43d irma-demo/RU/logo.png
d9f04a4b5ddf96e90d19c3baca10fcab2af56baa33eee7dcc9f6d10232f2b80a irma-demo/description.xml
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3FUAXtr8L/CT7WofXXcl7yiYI59r
z8ZSb+60UrkIn/ktBlOPlg1SYBNTXP4ITL0x0K4hHDF1DPXyH1F0rpVtCw==
-----END PUBLIC KEY-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJF++zlqPOPsLC2UnZ5wTbSX5eaEn8Jxlitl+nTG++cVoAoGCCqGSM49
AwEHoUQDQgAE3FUAXtr8L/CT7WofXXcl7yiYI59rz8ZSb+60UrkIn/ktBlOPlg1S
YBNTXP4ITL0x0K4hHDF1DPXyH1F0rpVtCw==
-----END EC PRIVATE KEY-----
\ No newline at end of file
c01fb73d8b9f3ae1e530f6335bbb1857d67d5022c5c25e5188f0dd9e0f688707 test/description.xml
f2253797426a9214532cdd1b976e3452498bd0a82236acd2a981db0405e8d37d test/test/Issues/mijnirma/description.xml
61a1fc7f161e43f8fc5b0c6ac2997cfe6bc0da7d27009b9914a04dca79ec6718 test/test/Issues/mijnirma/logo.png
3571e30777cdf5b97acbb0820f1b69983d2dc2b6bae91c8fb67cfc79ef4e2543 test/test/PublicKeys/0.xml
115bd71b738cd2ba10e832559fc5d35ee8222f4049a0fc10fc18d0853c4aa4d9 test/test/PublicKeys/1.xml
0e735a97da70dfe18b3dd3be8d75e5ee8499b626ba469a1c9556740ffd51ba03 test/test/PublicKeys/2.xml
a7f792bd702d6d97fd34d4104ddcf56bb3a0995e77fb36784099d4bd05c2df27 test/test/PublicKeys/3.xml
adc18a59954caeb907b999ab09b710c652665a0b150e5e4a7aff55199a4908dd test/test/description.xml
48f04181af6874a2f63f97d0a1a79b95f274da3e4d0efd9e5936b0ec0858b1cc test/test/logo.png
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3FUAXtr8L/CT7WofXXcl7yiYI59r
z8ZSb+60UrkIn/ktBlOPlg1SYBNTXP4ITL0x0K4hHDF1DPXyH1F0rpVtCw==
-----END PUBLIC KEY-----
......@@ -144,6 +144,10 @@ func (transport *HTTPTransport) GetBytes(url string) ([]byte, error) {
if err != nil {
return nil, &SessionError{ErrorType: ErrorTransport, Err: err}
}
if res.StatusCode != 200 {
return nil, &SessionError{ErrorType: ErrorServerResponse, Status: res.StatusCode}
}
b, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, &SessionError{ErrorType: ErrorServerResponse, Err: err, Status: res.StatusCode}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment