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) {
test.StartSchemeManagerHttpServer()
defer test.StopSchemeManagerHttpServer()
StartIrmaServer()
StartIrmaJwtServer()
defer StopIrmaServer()
test.CreateTestStorage(nil)
......
......@@ -41,6 +41,42 @@ func StopIrmaServer() {
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 {
client := parseStorage(t)
defer test.ClearTestStorage(t)
......
package sessiontest
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs"
"github.com/privacybydesign/irmago/internal/test"
......@@ -106,20 +109,8 @@ func getCombinedIssuanceRequest(id irma.AttributeTypeIdentifier) *irma.IssuanceR
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 {
var jwtcontents interface{}
var jwtcontents irma.RequestorJwt
switch url {
case "issue":
jwtcontents = irma.NewIdentityProviderJwt("testip", request.(*irma.IssuanceRequest))
......@@ -129,44 +120,54 @@ func getJwt(t *testing.T, request irma.SessionRequest, url string) string {
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)
bodybytes, err := json.Marshal(jwtcontents)
tok := jwt.NewWithClaims(jwt.SigningMethodRS256, jwtcontents)
tok.Header["kid"] = "testrequestor"
j, err := tok.SignedString(sk)
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 {
client = parseStorage(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
err := transport.Post("create", &qr, request)
err := server.Post("create", &qr, getJwt(t, request, sessiontype))
require.NoError(t, err)
qr.URL = "http://localhost:48682/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
token := qr.URL
qr.URL = url + "/irma/" + qr.URL
c := make(chan *SessionResult)
h := TestHandler{t, c, client}
j, err := json.Marshal(qr)
qrjson, err := json.Marshal(qr)
require.NoError(t, err)
client.NewSession(string(j), h)
client.NewSession(string(qrjson), h)
if result := <-c; result != nil {
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) {
......
......@@ -71,14 +71,15 @@ func JsonResponse(v interface{}, err *irma.RemoteError) (int, []byte) {
}
func WriteError(w http.ResponseWriter, err Error, msg string) {
status, bts := JsonResponse(nil, RemoteError(err, msg))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(bts)
WriteResponse(w, nil, RemoteError(err, msg))
}
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.WriteHeader(status)
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 (
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-chi/chi"
"github.com/go-errors/errors"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server"
"github.com/privacybydesign/irmago/server/irmarequestor"
)
type Configuration struct {
*server.Configuration
Port int
}
var s *http.Server
var (
s *http.Server
conf *Configuration
)
// Start the server. If successful then it will not return until Stop() is called.
func Start(conf *Configuration) error {
handler, err := Handler(conf.Configuration)
func Start(config *Configuration) error {
handler, err := Handler(config)
if err != nil {
return err
}
// Start server
s = &http.Server{Addr: fmt.Sprintf(":%d", conf.Port), Handler: handler}
s = &http.Server{Addr: fmt.Sprintf(":%d", config.Port), Handler: handler}
err = s.ListenAndServe()
if err == http.ErrServerClosed {
return nil // Server was closed normally
......@@ -40,8 +39,12 @@ func Stop() {
s.Close()
}
func Handler(conf *server.Configuration) (http.Handler, error) {
if err := irmarequestor.Initialize(conf); err != nil {
func Handler(config *Configuration) (http.Handler, error) {
conf = config
if err := irmarequestor.Initialize(conf.Configuration); err != nil {
return nil, err
}
if err := conf.initialize(); err != nil {
return nil, err
}
......@@ -54,6 +57,8 @@ func Handler(conf *server.Configuration) (http.Handler, error) {
router.Post("/create", handleCreate)
router.Get("/status/{token}", handleStatus)
router.Get("/result/{token}", handleResult)
router.Get("/result-jwt/{token}", handleJwtResult)
router.Get("/getproof/{token}", handleJwtProofs)
return router, nil
}
......@@ -64,12 +69,48 @@ func handleCreate(w http.ResponseWriter, r *http.Request) {
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
request, err := parseRequest(body)
if err != nil {
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
// Authenticate request: check if the requestor is known and allowed to submit requests