I'm making a little API using Go. I would like to be able to handle Github webhooks with secret token. I set up the secret on my webhook Github which is "azerty".
Now I try to verify that the incoming webhook has the correct secret token. I've read Github documentation which say the algorithm use HMAC with SHA1. But I can't verify the secret from the incoming Github webhook ...
func IsValidSignature(r *http.Request, key string) bool {
// KEY => azerty
gotHash := strings.SplitN(r.Header.Get("X-Hub-Signature"), "=", 2)
if gotHash[0] != "sha1" {
return false
}
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Cannot read the request body: %s\n", err)
return false
}
hash := hmac.New(sha1.New, []byte(key))
if _, err := hash.Write(b); err != nil {
log.Printf("Cannot compute the HMAC for request: %s\n", err)
return false
}
expectedHash := hex.EncodeToString(hash.Sum(nil))
log.Println("EXPECTED HASH:", expectedHash)
log.Println("GOT HASH:", gotHash[1])
return gotHash[1] == expectedHash
}
EXPECTED HASH: 10972179a3b0efc337f79ec41847062bc598bb04
GOT HASH: 36de72e0d386e36e2c7b034c85cd3b3889594992
To test, I copy the payload of the Github webhook in Postman with the right headers. I don't know why I get two different hash ... I've checked my key is non-empty with the correct value and my body is non-empty too.
Do I miss something?
I copy the payload of the Github webhook in Postman with the right header.
I've checked my key is non-empty with the correct value and my body is non-empty too.
The crypto is correct, except few minor issues. Obviously your body does not match the same body you have got from Github. Could be formatting, trailing newlines, etc. It must exactly byte-to-byte match the original body.
If this code works with Github and does not work with copy in Postman just replace X-Hub-Signature in your fixture with that "wrong" hash.
Some extras:
Use hmac.Equal for secure comparison
hash.Write never returns errors
Related
I am trying to implement iamcredentials Go API client to generate an Access Token to access some Google APIs via REST API, I am using this code
package main
import (
"context"
"log"
"google.golang.org/api/iamcredentials/v1"
)
func main() {
iamcredentialsService, err := iamcredentials.NewService(context.Background())
if err != nil {
log.Println("error initialize iamcredential Service ", err)
return
}
accessTokenCall := iamcredentialsService.Projects.ServiceAccounts.GenerateAccessToken(
"projects/-/serviceAccounts/some-sa#some-project-id.iam.gserviceaccount.com:generateAccessToken",
&iamcredentials.GenerateAccessTokenRequest{
Scope: []string{
iamcredentials.CloudPlatformScope,
},
},
)
iamResp, err := accessTokenCall.Do()
if err != nil {
log.Println("error generate access token", err)
return
}
log.Println(iamResp)
}
But when I tried to run the above snippet, I got this message
go run main.go
error generate access token googleapi: Error 400: Request contains an invalid argument., badRequest
Is there any way to check which one is causing the above response? I am not sure since there isn't any good example of implementation. Any help would be appreciated, Thanks.
Notes :
I have checked following documentation on this topic https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials and this https://pkg.go.dev/google.golang.org/api/iamcredentials/v1#pkg-overview
I have already setup the Service account using Service Account Token Creator role on IAM and also enabled the IAM API from the console
Also I have added GOOGLE_APPLICATION_CREDENTIALS to the environment variables as suggested
#DanielFarrell is right, you need to remove the :generateAccessToken at the end. Here the documentation in the code. Don't hesitate to explore it, it's open source ;)
// GenerateAccessToken: Generates an OAuth 2.0 access token for a
// service account.
//
// - name: The resource name of the service account for which the
// credentials are requested, in the following format:
// `projects/-/serviceAccounts/{ACCOUNT_EMAIL_OR_UNIQUEID}`. The `-`
// wildcard character is required; replacing it with a project ID is
// invalid.
func (r *ProjectsServiceAccountsService) GenerateAccessToken(name string, generateaccesstokenrequest *GenerateAccessTokenRequest) *ProjectsServiceAccountsGenerateAccessTokenCall {
c := &ProjectsServiceAccountsGenerateAccessTokenCall{s: r.s, urlParams_: make(gensupport.URLParams)}
c.name = name
c.generateaccesstokenrequest = generateaccesstokenrequest
return c
}
I'm using this example provided under cloud functions to make a GET request to another GCP API:
import (
"context"
"fmt"
"io"
"google.golang.org/api/idtoken"
)
func makeGetRequest(w io.Writer, targetURL string) error {
ctx := context.Background()
client, err := idtoken.NewClient(ctx, targetURL)
if err != nil {
return fmt.Errorf("idtoken.NewClient: %v", err)
}
resp, err := client.Get(targetURL)
if err != nil {
return fmt.Errorf("client.Get: %v", err)
}
defer resp.Body.Close()
if _, err := io.Copy(w, resp.Body); err != nil {
return fmt.Errorf("io.Copy: %v", err)
}
return nil
}
but when I log the request sent I don't see any authorization header and I get the following error:
"Request
had invalid authentication credentials. Expected OAuth 2 access token, login cookie
or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.\"
I have given serviceAccountTokenCreator and the target GCP API admin permissions to the service account that's used to create the cloud function.
Am I misunderstanding what the documentation is saying? It seems like the authorization header should be automatically added.
It might be easier for you to not build the request from scratch and use Client Libraries instead. It provides idiomatic, generated or hand-written code in each language, making the Cloud API simple and intuitive to use. It also handles authentication for you.
From what you're following, the client automatically adds an "Authorization" header so that shouldn't be the problem. You're also trying to follow an example that generates an Identity Token, because calling a Cloud Function endpoint that has authentication requires an Identity token. This is different on your use case, because calling GCP APIs require an OAuth 2 access token. This link explains the difference between the two.
There are ways to generate an access token programmatically such as getting them from the metadata server as I did in my other answer (it's in Python but you can also do it in Golang). However, I suggest learning more on how Client Libraries work and test it for yourself. There are many examples shown on GitHub to get you started.
I am just started learning Go, and this question made me stuck.
Trying to test request handling on localhost in testing func using github.com/valyala/fasthttp.
First running the server like in https://github.com/valyala/fasthttp/blob/master/server_example_test.go:
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("error in net.Listen: %s", err)
}
requestHandler := func(ctx *fasthttp.RequestCtx) {
fmt.Println(ctx, "Requested path is")
}
if err := fasthttp.Serve(ln, requestHandler); err != nil {
log.Fatalf("error in Serve: %s", err)
}
then if I run the request func (FastRequest(url string)) from the same testing function it works fine...
Fasthttp request func:
func FastRequest(url string) error {
Request := &fasthttp.Request{}
Response := &fasthttp.Response{}
FastHTTPClient := &fasthttp.Client{}
Request.SetRequestURI(url)
for {
err := FastHTTPClient.DoTimeout(Request, Response, time.Minute)
switch err {
case fasthttp.ErrTimeout, fasthttp.ErrDialTimeout:
<-time.After(time.Minute * 2)
continue
case fasthttp.ErrNoFreeConns:
<-time.After(time.Minute * 2)
continue
case nil:
return nil
default:
if strings.Contains(err.Error(), "connection reset by peer") {
<-time.After(time.Minute * 2)
continue
} else {
return err
}
}
}
}
But what I truly need to test is sending a request from my object, which implements the same FastRequest method in goroutine.
And here I've got this error message:
error when serving connection ":8080"<->":51325": error when reading request headers: invalid header key " http/1.1\r\nuser-Agent". Buffer size=206, contents: "GET here_is_request_url \n http/1.1\r\nuser-Agent: fasthttp\r\nHost: localhost:8080\r\n\r\n"
In FastRequest I haven't specified any user agent and the functions FastRequest() are the same. Only the place where the function is called is different. Whether it's called in goroutine or not does not matter.
So, fasthttp.RequestCtx cannot parse its own header? or what is going on?
==========================================================================
Also, I should have added that in first case I've used fasthttp v1.6.0, when I changed it to 1.8.0 the error was:
error when serving connection ":8080"<->":57093": error when reading request headers: invalid header name. Buffer size=215, contents: "GET here_is_request_url\n HTTP/1.1\r\nUser-Agent: fasthttp\r\nHost: localhost:8080\r\n\r\n"
And finally, the issue was in "/n" added at the end of the url, that works for real servers, by my small localhost server couldn't handle it.
... contents: "GET here_is_request_url \n http/1.1\r\n ...
^^^^^^^^^^^^^
WRONG!
The URL you use in your code likely has a newline character (\n) still at the end since this is included in your request before the HTTP version and thus messes up the HTTP request. A real URL should have no white space which includes spaces and also newline characters.
Additionally the HTTP version should be all-uppercase HTTP/1.1, i.e. your lower-case http/1.1 is wrong too. You don't show how you create the HTTP request but it is very likely messed up.
My use case looks like I know the public certificates of my clients and only want to allow them. I have a go server based on gin and a TLS configuration in which I have assigned a method to the property "VerifyPeerCertificate".
The function looks like
func customVerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(verifiedChains) < 1 {
return errors.New("Verified certificate chains is empty.")
}
if len(verifiedChains[0]) < 1 {
return errors.New("No certificates in certificate chains.")
}
if len(verifiedChains[0][0].Subject.CommonName) < 1 {
return errors.New("Common name can not be empty.")
}
fmt.Println(verifiedChains[0][0].Raw)
publicKeyDer, _ := x509.MarshalPKIXPublicKey(verifiedChains[0][0].PublicKey)
publicKeyBlock := pem.Block{
Type: "CERTIFICATE",
Bytes: publicKeyDer,
}
publicKeyPem := string(pem.EncodeToMemory(&publicKeyBlock))
}
The problem is, however, that the string in the variable "publicKeyPem" does not look like the client public certificate I used to send the request to the server, it's also shorter in length.
A certificate is more than its public key. The entire x509.Certificate object represents the certificate presented by the client, the public key field is only the actual value of the public key.
If you want to compare certificates for strict equality, you should use the rawCerts [][]byte argument passed to your callback. This is mentioned in the tls.Config comments for VerifyPeerCertificate:
VerifyPeerCertificate, if not nil, is called after normal
certificate verification by either a TLS client or server. It
receives the raw ASN.1 certificates provided by the peer and also
any verified chains that normal processing found. If it returns a
non-nil error, the handshake is aborted and that error results.
Thanks to Marc, I know that I used the wrong variable. To convert the certificate as a string, as used by the client, use the following code
publicKeyBlock := pem.Block{
Type: "CERTIFICATE",
Bytes: rawCerts[0],
}
publicKeyPem := string(pem.EncodeToMemory(&publicKeyBlock))
I am trying to upload a CustomSchema to all Users of a company in GSuite. This Custom Schema contains their Github Usernames, which I extracted with the github API.
The problem is, after running the code, the account in Gsuite is not added.
Relevant code (A connection to GSuite with admin Authentication is established, the map has all user entries. If you still want more code, I can provide you with it - just trying to keep it simple):
for _, u := range allUsers.Users {
if u.CustomSchemas != nil {
log.Printf("%v", string(u.CustomSchemas["User_Names"]))
}else{
u.CustomSchemas = map[string]googleapi.RawMessage{}
}
nameFromGsuite := u.Name.FullName
if githubLogin, ok := gitHubAccs[nameFromGsuite]; ok {
userSchemaForGithub := GithubAcc{GitHub: githubLogin}
jsonRaw, err := json.Marshal(userSchemaForGithub)
if err != nil {
log.Fatalf("Something went wrong logging: %v", err)
}
u.CustomSchemas["User_Names"] = jsonRaw
adminService.Users.Update(u.Id, u)
} else {
log.Printf("User not found for %v\n", nameFromGsuite)
}
}
This is the struct for the json encoding:
type GithubAcc struct {
GitHub string `json:"GitHub"`
}
For anyone stumbling upon this.
Everything in the code snippet is correct. By the way the method is written, I expected that adminService.Users.Update() actually updates the user. Instead, it returns an UserUpdatesCall.
You need to execute that update by calling .Do()
From the API:
Do executes the "directory.users.update" call.
So the solution is to change adminService.Users.Update(u.Id, u)
into adminService.Users.Update(u.Id, u).Do()