diff --git a/api/account.go b/api/account.go index eea257c..ea5cda7 100644 --- a/api/account.go +++ b/api/account.go @@ -6,30 +6,18 @@ import ( "encoding/pem" "bytes" + "crypto/x509" "fmt" "io/ioutil" "net/http" - "crypto/x509" - "github.com/gorilla/mux" "github.com/duo-labs/webauthn/protocol" + "github.com/gorilla/mux" - "github.com/Usable-Security-and-Privacy-Lab/lets-auth-ca/models" "github.com/Usable-Security-and-Privacy-Lab/lets-auth-ca/certs" + "github.com/Usable-Security-and-Privacy-Lab/lets-auth-ca/models" ) -type AuthKeyRequest struct { - AuthPublicKey string `json:"authPublicKey"` -} - -type CSRRequest struct { - CSR string `json:"CSR"` -} - -type CertificateResponse struct { - Certificate string `json:"certificate"` -} - func CreateBegin(w http.ResponseWriter, r *http.Request) { fmt.Printf("Started CreateBegin request %s\n", r.RequestURI) defer fmt.Printf("Finished CreateBegin request %s\n", r.RequestURI) @@ -90,7 +78,6 @@ func CreateBegin(w http.ResponseWriter, r *http.Request) { return } - jsonResponse(w, options, http.StatusOK) } @@ -154,10 +141,10 @@ func CreateFinish(w http.ResponseWriter, r *http.Request) { auth := models.MakeAuthenticator(&credential.Authenticator) credentialID := base64.URLEncoding.EncodeToString(credential.ID) c := &models.Credential{ - Auth: auth, - PublicKey: credential.PublicKey, - CredentialID: credentialID, - UserID: user.ID, + Auth: auth, + PublicKey: credential.PublicKey, + CredentialID: credentialID, + UserID: user.ID, } err = models.CreateCredential(c) if err != nil { @@ -172,7 +159,7 @@ func CreateFinish(w http.ResponseWriter, r *http.Request) { // print it var bodyBytes []byte fmt.Println("request:") - bodyBytes, err = ioutil.ReadAll(r.Body) + bodyBytes, err = ioutil.ReadAll(r.Body) var prettyJSON bytes.Buffer if err = json.Indent(&prettyJSON, bodyBytes, "", "\t"); err != nil { fmt.Printf("JSON parse error: %v", err) @@ -198,10 +185,9 @@ func CreateFinish(w http.ResponseWriter, r *http.Request) { fmt.Println("got key", request.AuthPublicKey) - // Store the authenticator public key authKey := &models.AuthKey{ - Key: request.AuthPublicKey, + Key: request.AuthPublicKey, UserID: user.ID, } err = models.CreateAuthKey(authKey) @@ -219,14 +205,14 @@ func CreateFinish(w http.ResponseWriter, r *http.Request) { func SignCSR(w http.ResponseWriter, r *http.Request) { fmt.Printf("Started SignCSR request %s\n", r.RequestURI) defer fmt.Printf("Finished SignCSR request %s\n", r.RequestURI) - + vars := mux.Vars(r) username, ok := vars["username"] if !ok { jsonResponse(w, fmt.Errorf("must supply a valid username i.e. foo@bar.com"), http.StatusBadRequest) return } - + user, err := models.GetUserByUsername(username) if err != nil { // user isn't in database @@ -234,7 +220,7 @@ func SignCSR(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) return } - + // Get the CSR from the request var request CSRRequest err = json.NewDecoder(r.Body).Decode(&request) @@ -270,8 +256,8 @@ func SignCSR(w http.ResponseWriter, r *http.Request) { // Second, convert the public key in the CSR into PEM format publicKeyDer, _ := x509.MarshalPKIXPublicKey(csr.PublicKey) publicKeyBlock := pem.Block{ - Type: "PUBLIC KEY", - Bytes: publicKeyDer, + Type: "PUBLIC KEY", + Bytes: publicKeyDer, } publicKey := string(pem.EncodeToMemory(&publicKeyBlock)) @@ -317,4 +303,3 @@ func jsonResponse(w http.ResponseWriter, d interface{}, c int) { w.WriteHeader(c) fmt.Fprintf(w, "%s", dj) } - diff --git a/api/api.go b/api/api.go index e66a172..21d8bd5 100644 --- a/api/api.go +++ b/api/api.go @@ -3,25 +3,35 @@ package api import ( "log" - "github.com/duo-labs/webauthn/webauthn" "github.com/Usable-Security-and-Privacy-Lab/lets-auth-ca/util" + "github.com/duo-labs/webauthn/webauthn" "github.com/go-playground/validator/v10" ) - var webAuthn *webauthn.WebAuthn var sessionStore *Store var validate *validator.Validate +type AuthKeyRequest struct { + AuthPublicKey string `json:"authPublicKey"` +} + +type CSRRequest struct { + CSR string `json:"CSR"` +} + +type CertificateResponse struct { + Certificate string `json:"certificate"` +} func Init() { var err error cfg := util.GetConfig() webAuthn, err = webauthn.New(&webauthn.Config{ - RPDisplayName: cfg.RPDisplayName, // Display Name for your site - RPID: cfg.RPID, // Generally the domain name for your site - RPOrigin: cfg.RPOrigin, // this needs to be the origin for the request, with the protocol (HTTP(S)) and port number (if not 80 for HTTP or 443 for HTTPS) + RPDisplayName: cfg.RPDisplayName, // Display Name for your site + RPID: cfg.RPID, // Generally the domain name for your site + RPOrigin: cfg.RPOrigin, // this needs to be the origin for the request, with the protocol (HTTP(S)) and port number (if not 80 for HTTP or 443 for HTTPS) }) if err != nil { log.Fatal("failed to create WebAuthn from config:", err) @@ -31,6 +41,6 @@ func Init() { if err != nil { log.Fatal("failed to create session store:", err) } - + validate = validator.New() -} \ No newline at end of file +} diff --git a/api/user.go b/api/user.go new file mode 100644 index 0000000..b60e881 --- /dev/null +++ b/api/user.go @@ -0,0 +1,149 @@ +package api + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + + "github.com/Usable-Security-and-Privacy-Lab/lets-auth-ca/certs" + "github.com/Usable-Security-and-Privacy-Lab/lets-auth-ca/models" + "github.com/gorilla/mux" +) + +// certificateRequest represents the CSR for a single certificate +type certificateRequest struct { + CSR string `json:"CSR"` + AuthSignature string `json:"authSignature"` + AuthenticatorCertificate string `json:"authenticatorCertificate"` +} + +// serviceResponse represents the response made when the CA succesfully +// signs all the certificates +type serviceResponse struct { + SignedCertificate string `json:"serviceCertificate"` +} + +func ObtainAccountCertificate(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Beginning serving request %s\n", r.RequestURI) + defer fmt.Printf("Finished serving request %s\n", r.RequestURI) + + pathVariables := mux.Vars(r) + username := pathVariables["username"] + + reqBody, _ := ioutil.ReadAll(r.Body) + var request certificateRequest + json.Unmarshal(reqBody, &request) + + // Verify Authenticator Signature + authCert, CSR, signature := request.AuthenticatorCertificate, request.CSR, request.AuthSignature + + user, err := models.GetUserByUsername(username) + if err != nil { + // user isn't in database + fmt.Printf("User is not in database") + w.WriteHeader(http.StatusUnauthorized) + return + } + + // Sign the Certificate + byteCSR := []byte(CSR) + cert, _ := pem.Decode(byteCSR) + var csr *x509.CertificateRequest + + // var err error + if cert == nil { + csr, err = x509.ParseCertificateRequest(byteCSR) + // checkError(err) + } else { + csr, err = x509.ParseCertificateRequest(cert.Bytes) + // checkError(err) + } + if err != nil { + fmt.Println("CSR bad format", err.Error()) + jsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + // Check that the CSR is for one of the valid authenticator public keys + // First, get the authorized keys for this user + authKeys, err := models.GetAuthKeysForUser(user) + if err != nil { + jsonResponse(w, "unable to get authenticator keys for this user", http.StatusInternalServerError) + return + } + // Second, convert the public key in the CSR into PEM format + publicKeyDer, _ := x509.MarshalPKIXPublicKey(csr.PublicKey) + publicKeyBlock := pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyDer, + } + publicKey := string(pem.EncodeToMemory(&publicKeyBlock)) + + // Third, check if the key matches any of the valid keys for this user + present := models.AuthKeyPresent(publicKey, authKeys) + if !present { + jsonResponse(w, "authenticator key is not authorized for this account", http.StatusUnauthorized) + return + } + + // TBD we need to validate the CSR. This should include being sure it is properly signed. + // It is for the username this account owns. + // What else? + if username != csr.Subject.CommonName { + jsonResponse(w, "username doesn't match CSR subject", http.StatusBadRequest) + return + } + + signedCert, err := certs.SignAuthCertificate(csr) + pemCert := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: signedCert.Raw})) + + if err != nil { + switch { + case pemCert == "Invalid Device Cert": + w.WriteHeader(403) + fmt.Fprint(w, pemCert) + fmt.Println("403, Invalid Device Cert") + // Do these need returns? ask if the old Login endpoint was exhaustively tested + default: + w.WriteHeader(500) + fmt.Fprint(w, pemCert) + fmt.Printf("500, %s\n", pemCert) + } + return + } + + err = models.VerifyAccountID(signedCert, username) + if err != nil { + if err.Error() == "account already taken by other user" || err.Error() == "account already exists by current user" { + w.WriteHeader(403) + } else { + w.WriteHeader(500) + } + } + + err = certs.VerifyRSASignatureFromCert(signedCert, byteCSR, signature) + // TODO: AuthenticatorCertificate must be signed by CA, and for signed this account + if err != nil { + jsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + /* + // Maybe we don't need this statement + // Add the certificate to the database + err = certs.AddCert(signedCert, authCert) + if err != nil { + // AGAIN ASK FOR CLERIFICATION + jsonResponse(w, fmt.Errorf("failed adding cert to certs table"), http.StatusInternalServerError) + return + } + */ + + response := serviceResponse{SignedCertificate: pemCert} + final, _ := json.Marshal(response) + + fmt.Fprint(w, string(final)) +} diff --git a/api/username_test.go b/api/username_test.go new file mode 100644 index 0000000..ffa9deb --- /dev/null +++ b/api/username_test.go @@ -0,0 +1,17 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/mock" +) + +type mockObtainAccountCertificate struct { + mock.Mock +} + +// TODO: implement tests + +func TestObtainAccountCertificate(t *testing.T) { + +} diff --git a/certs/rsa.go b/certs/rsa.go new file mode 100644 index 0000000..3de408a --- /dev/null +++ b/certs/rsa.go @@ -0,0 +1,87 @@ +package certs + +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + + "gorm.io/gorm" +) + +// VerifyRSASignatureFromCert verifies an RSA Signature (with SHA-256) for some string data +func VerifyRSASignatureFromCert(cert *x509.Certificate, message []byte, base64Signature string) error { + // get signature from base64 into bytes + signature, err := base64.StdEncoding.DecodeString(base64Signature) + + if err != nil { + return err + } + + // get message into bytes for hashing + hashed := sha256.Sum256(message) + + // get the public key from the certificate + key := cert.PublicKey + + // dot the actual verification of the signature + return rsa.VerifyPKCS1v15(key.(*rsa.PublicKey), crypto.SHA256, hashed[:], signature) +} + +// AddCert adds a new service certificate to the database, along with it's association to a device certificate +func AddCert(certString, deviceCert string) error { + + pemBlock, _ := pem.Decode([]byte(certString)) + if pemBlock == nil { + return errors.New("invalid PEM string for Certificate") + } + + // Parse and return pemBlock + certificate, err := x509.ParseCertificate(pemBlock.Bytes) + + if err != nil { + return errors.New("unable to convert certificate FromPem in AddNewCert") + } + + notBefore := certificate.NotBefore + expTime := certificate.NotAfter + + var pubKey []byte + if certificate != nil { + pubKey = pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(certificate.PublicKey.(*rsa.PublicKey)), + }, + ) + } else { + return errors.New("unable to obtain public key from cert in AddNewCert") + } + + var db gorm.DB + + // first try to update the certificate in the database + sql := "UPDATE certs SET cert=?, certExp=?, lastUpdated=? WHERE pubKey=?" + if err = db.Exec(sql, certString, expTime, notBefore, pubKey).Error; err != nil { + fmt.Println("Error updating cert in cert table") + fmt.Println(err) + return errors.New("unable to update certificate in the database") + } + + // next insert into the database if the certificate wasn't previously stored + sql = "INSERT IGNORE certs(cert, eblob, certExp, pubKey, deviceCertId, lastUpdated)\n" + sql += "SELECT ?, ?, ?, ?, userDevice.id, ? FROM userDevice WHERE deviceCert=?" + + err = db.Exec(sql, certString, "a blob", expTime, pubKey, notBefore, deviceCert).Error + + if err != nil { + fmt.Println(err) + return errors.New("error inserting into cert table") + } + + return nil +} diff --git a/main.go b/main.go index 83f8675..12c231b 100644 --- a/main.go +++ b/main.go @@ -58,6 +58,7 @@ func main() { router.HandleFunc("/la3/account/create-begin/{username}", api.CreateBegin).Methods("GET") router.HandleFunc("/la3/account/create-finish/{username}", api.CreateFinish).Methods("POST") router.HandleFunc("/la3/account/sign-csr/{username}", api.SignCSR).Methods("POST") + router.HandleFunc("/la3/user/{username}/account", api.ObtainAccountCertificate).Methods("POST") url := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) diff --git a/models/accountmap.go b/models/accountmap.go new file mode 100644 index 0000000..f514426 --- /dev/null +++ b/models/accountmap.go @@ -0,0 +1,61 @@ +package models + +import ( + "crypto/x509" + "errors" + "fmt" + + "gorm.io/gorm" +) + +type AccountMap struct { + gorm.Model + + username string + accountID uint +} + +func MakeAccoutMap(username string, accountID uint) AccountMap { + accountMap := AccountMap{ + username: username, + accountID: accountID, + } + return accountMap +} + +func CreateAccountMap(a *AccountMap) error { + err := db.Create(&a).Error + return err +} + +func VerifyAccountID(certificate *x509.Certificate, username string) error { + // acc := AccountMap{} + accountID := certificate.Subject.CommonName + + var exists int + var err error + + err = db.Where("accountID=? AND username!=?", accountID, username).Scan(&exists).Error + + if err != nil { + fmt.Println(err) + return errors.New("database error") + } + + if exists == 1 { + return errors.New("account already taken by other user") + } + + err = db.Where("accountID=? AND username=?", accountID, username).Scan(&exists).Error + + if err != nil { + fmt.Println(err) + return errors.New("database error") + } + + if exists == 1 { + return errors.New("account already exists by current user") + } + + return nil +} diff --git a/models/authkeys.go b/models/authkeys.go index dec56f2..02f5e78 100644 --- a/models/authkeys.go +++ b/models/authkeys.go @@ -2,6 +2,7 @@ package models import ( "strings" + "gorm.io/gorm" ) @@ -12,7 +13,7 @@ import ( type AuthKey struct { gorm.Model - Key string + Key string UserID uint } @@ -31,9 +32,9 @@ func GetAuthKeysForUser(user User) ([]AuthKey, error) { return authKeys, err } -func AuthKeyPresent(key string, authKeys []AuthKey) (bool) { +func AuthKeyPresent(key string, authKeys []AuthKey) bool { for i := 0; i < len(authKeys); i++ { - if authKeys[i].Key == key { + if authKeys[i].Key == key { return true } } @@ -44,4 +45,4 @@ func AuthKeyPresent(key string, authKeys []AuthKey) (bool) { // after they have logged in (so at the finish part of a FIDO2 login). func DeleteAuthKey(key string) error { return db.Where("key = ?", key).Delete(&AuthKey{}).Error -} \ No newline at end of file +} diff --git a/models/models.go b/models/models.go index 379d33f..0de02ac 100644 --- a/models/models.go +++ b/models/models.go @@ -4,12 +4,12 @@ package models // https://github.com/duo-labs/webauthn.io import ( + "database/sql" "encoding/binary" "errors" - "database/sql" - "gorm.io/gorm" "gorm.io/driver/mysql" + "gorm.io/gorm" "github.com/Usable-Security-and-Privacy-Lab/lets-auth-ca/util" ) @@ -19,7 +19,6 @@ var db *gorm.DB // ErrUsernameTaken is thrown when a user attempts to register a username that is taken. var ErrUsernameTaken = errors.New("username already taken") - // BytesToID converts a byte slice to a uint. This is needed because the // WebAuthn specification deals with byte buffers, while the primary keys in // our database are uints. @@ -33,7 +32,7 @@ func BytesToID(buf []byte) uint { // It also populates the Config object func Setup(config *util.Config) error { // assume the database is already created - + // Open our database connection temp_db, err := gorm.Open(mysql.Open(config.DbConfig), &gorm.Config{}) if err != nil { @@ -51,6 +50,7 @@ func Setup(config *util.Config) error { &User{}, &Credential{}, &AuthKey{}, + &AccountMap{}, ) if err != nil { @@ -59,4 +59,3 @@ func Setup(config *util.Config) error { return nil } -