|
package main |
|
|
|
import ( |
|
"bytes" |
|
"errors" |
|
"fmt" |
|
"log" |
|
"net/http" |
|
"strconv" |
|
"time" |
|
|
|
"github.com/ethereum/go-ethereum/common" |
|
"github.com/ethereum/go-ethereum/common/hexutil" |
|
"github.com/ethereum/go-ethereum/crypto" |
|
"github.com/ethereum/go-ethereum/ethclient" |
|
"github.com/gorilla/mux" |
|
"github.com/lithammer/shortuuid" |
|
|
|
"github.com/windingtree/go-orgid/organization" |
|
) |
|
|
|
var apiTokens = make(map[string]string) |
|
|
|
func main() { |
|
|
|
router := mux.NewRouter().StrictSlash(true) |
|
|
|
// GET /auth/ |
|
// Returns an ORG.ID authentication status at this server |
|
// Returns either an API token or an Unauthorized status (with error message) |
|
// curl -X GET http://127.0.0.1:8888/auth/ |
|
// -H 'X-ORG-ID: 0xFCea05bB6FBCC34Ae4115B7AE0C2c5E2EBA95eC0' |
|
// -H 'X-ORG-Associated-Key: 0xFCea05bB6FBCC34Ae4115B7AE0C2c5E2EBA95eC0' |
|
// -H 'X-ORG-ID-Challenge-Timestamp: 1570450590' |
|
// -H 'X-ORG-ID-Signature: 0x24953054e098dab97102a96dae87644c39001e8a5cd9a74a174f53183ef7f4dd59a15d81b2c4d0a60ec3d166122aa7c5464e455f4c748bf7bb5cd3a70c00255d1b' |
|
// "0xFCea..." is my ORG.ID, challenge is timestamp within 1 minute of current time, signature is an Ethereum signature of challenge |
|
router.HandleFunc("/auth/", getAuthHandler).Methods("GET") |
|
|
|
// POST /auth/ |
|
// Attempts to authenticate ORG.ID on this server |
|
// Returns a new API token |
|
router.HandleFunc("/auth/", postAuthHandler).Methods("POST") |
|
|
|
log.Fatal(http.ListenAndServe(":8888", router)) |
|
|
|
} |
|
|
|
func getAuthHandler(w http.ResponseWriter, r *http.Request) { |
|
oid := r.Header.Get("X-ORG-ID") |
|
_, status, err := isOrgIDValid(w, r) |
|
|
|
if err != nil { |
|
w.WriteHeader(status) |
|
fmt.Fprintf(w, err.Error()) |
|
return |
|
} |
|
|
|
// Check the "DB" for the API key |
|
if apiTokens[oid] == "" { |
|
w.WriteHeader(http.StatusNotFound) |
|
fmt.Fprintf(w, "API Token Not Found") |
|
} else { |
|
w.WriteHeader(http.StatusOK) |
|
fmt.Fprintf(w, apiTokens[oid]) |
|
} |
|
|
|
return |
|
} |
|
|
|
func postAuthHandler(w http.ResponseWriter, r *http.Request) { |
|
oid := r.Header.Get("X-ORG-ID") |
|
_, status, err := isOrgIDValid(w, r) |
|
|
|
if err != nil { |
|
w.WriteHeader(status) |
|
fmt.Fprintf(w, err.Error()) |
|
return |
|
} |
|
|
|
newAPIKey := shortuuid.New() |
|
apiTokens[oid] = newAPIKey |
|
|
|
w.WriteHeader(http.StatusOK) |
|
fmt.Fprintf(w, newAPIKey) |
|
} |
|
|
|
func isOrgIDValid(w http.ResponseWriter, r *http.Request) (bool, int, error) { |
|
// ORG.ID |
|
oid := r.Header.Get("X-ORG-ID") |
|
if !common.IsHexAddress(oid) { |
|
return false, http.StatusBadRequest, fmt.Errorf("ORG.ID is missing or invalid") |
|
} |
|
orgid := common.HexToAddress(oid) |
|
|
|
// Associated Key |
|
aKey := r.Header.Get("X-ORG-ID-Associated-Key") |
|
if !common.IsHexAddress(aKey) { |
|
return false, http.StatusBadRequest, fmt.Errorf("Associated key is missing or invalid") |
|
} |
|
associatedKey := common.HexToAddress(aKey) |
|
|
|
// Challenge timestamp |
|
challenge := r.Header.Get("X-ORG-ID-Challenge-Timestamp") |
|
|
|
t, err := strconv.ParseInt(challenge, 10, 64) |
|
if err != nil { |
|
return false, http.StatusBadRequest, fmt.Errorf("There was a problem with challenge timestamp. %v", err) |
|
} |
|
|
|
challengeTime := time.Unix(t, 0) |
|
now := time.Now() |
|
margin := now.Add(-time.Hour * 100) // Probably should be 60 seconds |
|
|
|
if !((challengeTime.After(margin) && challengeTime.Before(now)) || challengeTime.Equal(now)) { |
|
return false, http.StatusBadRequest, fmt.Errorf("Challenge timestamp is older than 60 minutes: %v", challengeTime) |
|
} |
|
|
|
// Signature |
|
verified, err := verifySignature(associatedKey, challenge, r.Header.Get("X-ORG-ID-Signature")) |
|
if err != nil { |
|
return false, http.StatusBadRequest, fmt.Errorf("There was a problem with signature verification. %v", err) |
|
} |
|
|
|
if !verified { |
|
return false, http.StatusUnauthorized, fmt.Errorf("ORG.ID authorization was unsuccessful") |
|
} |
|
|
|
// Check whether the ORG.ID lists the associated key it tries to provide |
|
client, err := ethclient.Dial("https://ropsten.infura.io") |
|
if err != nil { |
|
log.Fatal(err) |
|
return false, http.StatusInternalServerError, fmt.Errorf("Could not connect to Ropsten") |
|
} |
|
|
|
org, err := organization.NewOrganization(orgid, client) |
|
if err != nil { |
|
log.Panic(err) |
|
return false, http.StatusInternalServerError, fmt.Errorf("Problem connecting to the smart contract") |
|
} |
|
|
|
hasKey, err := org.HasAssociatedKey(nil, associatedKey) |
|
if err != nil { |
|
log.Panic(err) |
|
return false, http.StatusInternalServerError, fmt.Errorf("Problem connecting to the smart contract") |
|
} |
|
|
|
if !hasKey { |
|
return false, http.StatusUnauthorized, fmt.Errorf("The key does not belong to the ORG.ID") |
|
} |
|
|
|
return true, http.StatusOK, nil |
|
} |
|
|
|
func verifySignature(from common.Address, message string, signature string) (bool, error) { |
|
msg := []byte(message) |
|
sig := hexutil.MustDecode(signature) |
|
|
|
if bytes.NewReader(sig).Len() != 65 { |
|
return false, fmt.Errorf("The signature was expected to have 65 bytes, got %d intead", bytes.NewReader(sig).Len()) |
|
} |
|
|
|
// https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442 |
|
if sig[64] != 27 && sig[64] != 28 { |
|
return false, errors.New("The signature must conform to the secp256k1 curve R, S and V values, where the V value must be be 27 or 28 for legacy reasons. https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442") |
|
} |
|
sig[64] -= 27 |
|
|
|
// Recover public key from the signature |
|
pubKey, err := crypto.SigToPub(signHash(msg), sig) |
|
if err != nil { |
|
return false, errors.New("There was a problem recovering public key from the signature") |
|
} |
|
|
|
recoveredAddr := crypto.PubkeyToAddress(*pubKey) |
|
|
|
return from == recoveredAddr, nil |
|
} |
|
|
|
// This is how message signing in Ethereum works, don't ask |
|
func signHash(data []byte) []byte { |
|
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) |
|
return crypto.Keccak256([]byte(msg)) |
|
} |