Commit 1c08fbb2 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Add requestor authentication using jwt's and preshared keys


Co-authored-by: Tomas's avatarConfiks <confiks@scriptbase.org>
parent 938887a0
...@@ -15,7 +15,7 @@ func TestMain(m *testing.M) { ...@@ -15,7 +15,7 @@ func TestMain(m *testing.M) {
test.StartSchemeManagerHttpServer() test.StartSchemeManagerHttpServer()
defer test.StopSchemeManagerHttpServer() defer test.StopSchemeManagerHttpServer()
StartIrmaServer() StartIrmaJwtServer()
defer StopIrmaServer() defer StopIrmaServer()
test.CreateTestStorage(nil) test.CreateTestStorage(nil)
......
...@@ -41,6 +41,42 @@ func StopIrmaServer() { ...@@ -41,6 +41,42 @@ func StopIrmaServer() {
irmaserver.Stop() irmaserver.Stop()
} }
func StartIrmaJwtServer() {
testdata := test.FindTestdataFolder(nil)
logger := logrus.New()
logger.Level = logrus.WarnLevel
logger.Formatter = &logrus.TextFormatter{}
go func() {
err := irmaserver.Start(&irmaserver.Configuration{
Configuration: &server.Configuration{
Logger: logger,
IrmaConfigurationPath: filepath.Join(testdata, "irma_configuration"),
IssuerPrivateKeysPath: filepath.Join(testdata, "privatekeys"),
},
Port: 48682,
AuthenticateRequestors: true,
GlobalPermissions: irmaserver.Permissions{
Disclosing: []string{"*"},
Signing: []string{"*"},
Issuing: []string{"*"},
},
Requestors: map[string]irmaserver.Requestor{
"testrequestor": irmaserver.Requestor{
AuthenticationMethod: irmaserver.AuthenticationMethodPublicKey,
AuthenticationKey: filepath.Join(testdata, "jwtkeys", "testrequestor.pem"),
},
},
PrivateKey: filepath.Join(testdata, "jwtkeys", "sk.pem"),
})
if err != nil {
panic("Starting server failed: " + err.Error())
}
}()
time.Sleep(100 * time.Millisecond) // Give server time to start
}
func serverSessionHelper(t *testing.T, request irma.SessionRequest) *server.SessionResult { func serverSessionHelper(t *testing.T, request irma.SessionRequest) *server.SessionResult {
client := parseStorage(t) client := parseStorage(t)
defer test.ClearTestStorage(t) defer test.ClearTestStorage(t)
......
package sessiontest package sessiontest
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"testing" "testing"
"time" "time"
"github.com/dgrijalva/jwt-go"
"github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs" "github.com/privacybydesign/irmago/internal/fs"
"github.com/privacybydesign/irmago/internal/test" "github.com/privacybydesign/irmago/internal/test"
...@@ -106,20 +109,8 @@ func getCombinedIssuanceRequest(id irma.AttributeTypeIdentifier) *irma.IssuanceR ...@@ -106,20 +109,8 @@ func getCombinedIssuanceRequest(id irma.AttributeTypeIdentifier) *irma.IssuanceR
return request return request
} }
// startSession starts an IRMA session by posting the request,
// and retrieving the QR contents from the specified url.
func startSession(request interface{}, url string) (*irma.Qr, error) {
server := irma.NewHTTPTransport(url)
var response irma.Qr
err := server.Post("", &response, request)
if err != nil {
return nil, err
}
return &response, nil
}
func getJwt(t *testing.T, request irma.SessionRequest, url string) string { func getJwt(t *testing.T, request irma.SessionRequest, url string) string {
var jwtcontents interface{} var jwtcontents irma.RequestorJwt
switch url { switch url {
case "issue": case "issue":
jwtcontents = irma.NewIdentityProviderJwt("testip", request.(*irma.IssuanceRequest)) jwtcontents = irma.NewIdentityProviderJwt("testip", request.(*irma.IssuanceRequest))
...@@ -129,44 +120,54 @@ func getJwt(t *testing.T, request irma.SessionRequest, url string) string { ...@@ -129,44 +120,54 @@ func getJwt(t *testing.T, request irma.SessionRequest, url string) string {
jwtcontents = irma.NewSignatureRequestorJwt("testsigclient", request.(*irma.SignatureRequest)) jwtcontents = irma.NewSignatureRequestorJwt("testsigclient", request.(*irma.SignatureRequest))
} }
headerbytes, err := json.Marshal(&map[string]string{"alg": "none", "typ": "JWT"}) skbts, err := ioutil.ReadFile(filepath.Join(test.FindTestdataFolder(t), "jwtkeys", "testrequestor-sk.pem"))
require.NoError(t, err)
sk, err := jwt.ParseRSAPrivateKeyFromPEM(skbts)
require.NoError(t, err) require.NoError(t, err)
bodybytes, err := json.Marshal(jwtcontents) tok := jwt.NewWithClaims(jwt.SigningMethodRS256, jwtcontents)
tok.Header["kid"] = "testrequestor"
j, err := tok.SignedString(sk)
require.NoError(t, err) require.NoError(t, err)
return base64.RawStdEncoding.EncodeToString(headerbytes) + "." + base64.RawStdEncoding.EncodeToString(bodybytes) + "." return j
} }
func sessionHelper(t *testing.T, request irma.SessionRequest, url string, client *irmaclient.Client) { func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string, client *irmaclient.Client) {
if client == nil { if client == nil {
client = parseStorage(t) client = parseStorage(t)
defer test.ClearTestStorage(t) defer test.ClearTestStorage(t)
} }
transport := irma.NewHTTPTransport("http://localhost:48682") //transport := irma.NewHTTPTransport("http://localhost:48682")
//var qr irma.Qr
//err := transport.Post("create", &qr, request)
//require.NoError(t, err)
//qr.URL = "http://localhost:48682/irma/" + qr.URL
url := "http://localhost:48682"
server := irma.NewHTTPTransport(url)
var qr irma.Qr var qr irma.Qr
err := transport.Post("create", &qr, request) err := server.Post("create", &qr, getJwt(t, request, sessiontype))
require.NoError(t, err) require.NoError(t, err)
qr.URL = "http://localhost:48682/irma/" + qr.URL token := qr.URL
qr.URL = url + "/irma/" + qr.URL
//jwt := getJwt(t, request, url)
//url = "http://localhost:8088/irma_api_server/api/v2/" + url
//qr, transportErr := startSession(jwt, url)
//if transportErr != nil {
// fmt.Printf("+%v\n", transportErr)
//}
//require.NoError(t, transportErr)
//qr.URL = url + "/" + qr.URL
c := make(chan *SessionResult) c := make(chan *SessionResult)
h := TestHandler{t, c, client} h := TestHandler{t, c, client}
j, err := json.Marshal(qr) qrjson, err := json.Marshal(qr)
require.NoError(t, err) require.NoError(t, err)
client.NewSession(string(j), h) client.NewSession(string(qrjson), h)
if result := <-c; result != nil { if result := <-c; result != nil {
require.NoError(t, result.Err) require.NoError(t, result.Err)
} }
bts, err := server.GetBytes("getproof/" + token)
require.NoError(t, err)
fmt.Println(string(bts))
bts, err = server.GetBytes("result/" + token)
require.NoError(t, err)
fmt.Println(string(bts))
} }
func keyshareSessions(t *testing.T, client *irmaclient.Client) { func keyshareSessions(t *testing.T, client *irmaclient.Client) {
......
...@@ -71,14 +71,15 @@ func JsonResponse(v interface{}, err *irma.RemoteError) (int, []byte) { ...@@ -71,14 +71,15 @@ func JsonResponse(v interface{}, err *irma.RemoteError) (int, []byte) {
} }
func WriteError(w http.ResponseWriter, err Error, msg string) { func WriteError(w http.ResponseWriter, err Error, msg string) {
status, bts := JsonResponse(nil, RemoteError(err, msg)) WriteResponse(w, nil, RemoteError(err, msg))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(bts)
} }
func WriteJson(w http.ResponseWriter, object interface{}) { func WriteJson(w http.ResponseWriter, object interface{}) {
status, bts := JsonResponse(object, nil) WriteResponse(w, object, nil)
}
func WriteResponse(w http.ResponseWriter, object interface{}, rerr *irma.RemoteError) {
status, bts := JsonResponse(object, rerr)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) w.WriteHeader(status)
w.Write(bts) w.Write(bts)
......
package irmaserver
import (
"crypto/rsa"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-errors/errors"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server"
)
type Authenticator interface {
Authenticate(
headers http.Header, body []byte,
) (applies bool, request irma.SessionRequest, requestor string, err *irma.RemoteError)
Initialize(requestors map[string]Requestor) error
}
type AuthenticationMethod string
const (
AuthenticationMethodPublicKey = "publickey"
AuthenticationMethodPSK = "psk"
AuthenticationMethodNone = "none"
)
type PublicKeyAuthenticator struct {
publickeys map[string]*rsa.PublicKey
}
type PresharedKeyAuthenticator struct {
presharedkeys map[string]string
}
type NilAuthenticator struct{}
var authenticators map[string]Authenticator
func (NilAuthenticator) Authenticate(
headers http.Header, body []byte,
) (bool, irma.SessionRequest, string, *irma.RemoteError) {
if headers.Get("Authentication") != "" || !strings.HasPrefix(headers.Get("Content-Type"), "application/json") {
return false, nil, "", nil
}
request, err := parseRequest(body)
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
}
return true, request, "", nil
}
func (NilAuthenticator) Initialize(requestors map[string]Requestor) error {
return nil
}
func (pkauth *PublicKeyAuthenticator) Authenticate(
headers http.Header, body []byte,
) (bool, irma.SessionRequest, string, *irma.RemoteError) {
if headers.Get("Authorization") != "" || !strings.HasPrefix(headers.Get("Content-Type"), "text/plain") {
return false, nil, "", nil
}
requestorJwt := string(body)
claims := &jwt.StandardClaims{}
token, err := jwt.ParseWithClaims(requestorJwt, claims, jwtPublicKeyExtractor(pkauth.publickeys))
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
}
if !claims.VerifyIssuedAt(time.Now().Unix(), true) {
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt not yet valid")
}
if time.Unix(claims.IssuedAt, 0).Add(10 * time.Minute).Before(time.Now()) { // TODO make configurable
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt too old")
}
// Read JWT contents
parsedJwt, err := irma.ParseRequestorJwt(claims.Subject, requestorJwt)
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
}
requestor := token.Header["kid"].(string) // presence in Header and type is already checked by jwtPublicKeyExtractor
return true, parsedJwt.SessionRequest(), requestor, nil
}
func (pkauth *PublicKeyAuthenticator) Initialize(requestors map[string]Requestor) error {
pkauth.publickeys = map[string]*rsa.PublicKey{}
for name, requestor := range requestors {
if requestor.AuthenticationMethod != AuthenticationMethodPublicKey {
continue
}
var bts []byte
var err error
if strings.HasPrefix(requestor.AuthenticationKey, "-----BEGIN") {
bts = []byte(requestor.AuthenticationKey)
}
if _, err := os.Stat(requestor.AuthenticationKey); err == nil {
bts, err = ioutil.ReadFile(requestor.AuthenticationKey)
if err != nil {
return err
}
}
if len(bts) == 0 {
return errors.Errorf("Requestor %s has invalid public key", name)
}
pk, err := jwt.ParseRSAPublicKeyFromPEM(bts)
if err != nil {
return err
}
pkauth.publickeys[name] = pk
}
return nil
}
func (pskauth *PresharedKeyAuthenticator) Authenticate(
headers http.Header, body []byte,
) (bool, irma.SessionRequest, string, *irma.RemoteError) {
auth := headers.Get("Authentication")
if auth == "" || !strings.HasPrefix(headers.Get("Content-Type"), "application/json") {
return false, nil, "", nil
}
requestor, ok := pskauth.presharedkeys[auth]
if !ok {
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "")
}
request, err := parseRequest(body)
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
}
return true, request, requestor, nil
}
func (pskauth *PresharedKeyAuthenticator) Initialize(requestors map[string]Requestor) error {
pskauth.presharedkeys = map[string]string{}
for name, requestor := range requestors {
if requestor.AuthenticationMethod != AuthenticationMethodPSK {
continue
}
if requestor.AuthenticationKey == "" {
return errors.Errorf("Requestor %s had no authentication key")
}
pskauth.presharedkeys[requestor.AuthenticationKey] = name
}
return nil
}
// Helper functions
// Given an (unauthenticated) jwt, return the public key against which it should be verified using the "kid" header
func jwtPublicKeyExtractor(publickeys map[string]*rsa.PublicKey) func(token *jwt.Token) (interface{}, error) {
return func(token *jwt.Token) (interface{}, error) {
var ok bool
kid, ok := token.Header["kid"]
if !ok {
return nil, errors.New("No kid jwt header found")
}
requestor, ok := kid.(string)
if !ok {
return nil, errors.New("kid jwt header was not a string")
}
if pk, ok := publickeys[requestor]; ok {
return pk, nil
}
return nil, errors.Errorf("Unknown requestor: %s", requestor)
}
}
func parseRequest(bts []byte) (request irma.SessionRequest, err error) {
request = &irma.DisclosureRequest{}
if err = irma.UnmarshalValidate(bts, request); err == nil {
return request, nil
}
request = &irma.SignatureRequest{}
if err = irma.UnmarshalValidate(bts, request); err == nil {
return request, nil
}
request = &irma.IssuanceRequest{}
if err = irma.UnmarshalValidate(bts, request); err == nil {
return request, nil
}
return nil, errors.New("Invalid or disabled session type")
}
package irmaserver
import (
"crypto/rsa"
"io/ioutil"
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs"
"github.com/privacybydesign/irmago/server"
)
type Configuration struct {
*server.Configuration
Port int
AuthenticateRequestors bool
Requestors map[string]Requestor
GlobalPermissions Permissions
JwtIssuer string
PrivateKey string
privateKey *rsa.PrivateKey
}
type Permissions struct {
Disclosing []string
Signing []string
Issuing []string
}
type Requestor struct {
Permissions
AuthenticationMethod AuthenticationMethod
AuthenticationKey string
}
func contains(strings []string, query string) bool {
for _, s := range strings {
if s == query {
return true
}
}
return false
}
func (conf *Configuration) CanIssue(requestor string, creds []*irma.CredentialRequest) (bool, string) {
permissions := append(conf.Requestors[requestor].Issuing, conf.GlobalPermissions.Issuing...)
for _, cred := range creds {
id := cred.CredentialTypeID
if contains(permissions, "*") ||
contains(permissions, id.Root()+".*") ||
contains(permissions, id.IssuerIdentifier().String()+".*") ||
contains(permissions, id.String()) {
continue
} else {
return false, id.String()
}
}
return true, ""
}
func (conf *Configuration) CanVerifyOrSign(requestor string, action irma.Action, disjunctions irma.AttributeDisjunctionList) (bool, string) {
var permissions []string
switch action {
case irma.ActionDisclosing:
permissions = append(conf.Requestors[requestor].Disclosing, conf.GlobalPermissions.Disclosing...)
case irma.ActionIssuing:
permissions = append(conf.Requestors[requestor].Disclosing, conf.GlobalPermissions.Disclosing...)
case irma.ActionSigning:
permissions = append(conf.Requestors[requestor].Signing, conf.GlobalPermissions.Signing...)
}
if len(permissions) == 0 { // requestor is not present in the permissions
return false, ""
}
for _, disjunction := range disjunctions {
for _, attr := range disjunction.Attributes {
if contains(permissions, "*") ||
contains(permissions, attr.Root()+".*") ||
contains(permissions, attr.CredentialTypeIdentifier().IssuerIdentifier().String()+".*") ||
contains(permissions, attr.CredentialTypeIdentifier().String()+".*") ||
contains(permissions, attr.String()) {
continue
} else {
return false, attr.String()
}
}
}
return true, ""
}
func (conf *Configuration) initialize() error {
if err := conf.readPrivateKey(); err != nil {
return err
}
if !conf.AuthenticateRequestors {
conf.Logger.Warn("Requestor authentication disabled")
authenticators = map[string]Authenticator{AuthenticationMethodNone: NilAuthenticator{}}
// Leaving the global permission whitelists empty in this mode means enabling it for everyone
if len(conf.GlobalPermissions.Disclosing) == 0 {
conf.Logger.Info("No disclosing whitelist found: allowing verification of any attribute")
conf.GlobalPermissions.Disclosing = []string{"*"}
}
if len(conf.GlobalPermissions.Signing) == 0 {
conf.Logger.Info("No signing whitelist found: allowing attribute-based signature sessions with any attribute")
conf.GlobalPermissions.Signing = []string{"*"}
}
if len(conf.GlobalPermissions.Issuing) == 0 {
conf.Logger.Info("No issuance whitelist found: allowing issuance of any credential (for which private keys are installed)")
conf.GlobalPermissions.Issuing = []string{"*"}
}
return nil
}
authenticators = map[string]Authenticator{
AuthenticationMethodPublicKey: &PublicKeyAuthenticator{},
AuthenticationMethodPSK: &PresharedKeyAuthenticator{},
}
for _, authenticator := range authenticators {
if err := authenticator.Initialize(conf.Requestors); err != nil {
return err
}
}
return nil
}
func (conf *Configuration) readPrivateKey() error {
if conf.PrivateKey == "" {
return nil
}
var keybytes []byte
var err error
if strings.HasPrefix(conf.PrivateKey, "-----BEGIN") {
keybytes = []byte(conf.PrivateKey)
} else {
if err = fs.AssertPathExists(conf.PrivateKey); err != nil {
return err
}
if keybytes, err = ioutil.ReadFile(conf.PrivateKey); err != nil {
return err
}
}
conf.privateKey, err = jwt.ParseRSAPrivateKeyFromPEM(keybytes)
return err
}
...@@ -4,30 +4,29 @@ import ( ...@@ -4,30 +4,29 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/go-errors/errors"
"github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server" "github.com/privacybydesign/irmago/server"
"github.com/privacybydesign/irmago/server/irmarequestor" "github.com/privacybydesign/irmago/server/irmarequestor"
) )
type Configuration struct { var (
*server.Configuration s *http.Server
Port int conf *Configuration
} )