mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-02-15 10:55:14 +00:00
* Add configuration extension flags to server Add httpsignatures dependency Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Add http fetching to config fetcher Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Refetch config on rebuild Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * - Ensure multipipeline compatiblity - Send original config in http request Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Basic tests of config api Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Simple docs page Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Better flag naming Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Rename usages of the term yaml Rename ConfigAPI struct Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Doc adjustments Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * More docs touchups Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Fix env vars in docs Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * fix json tags for api calls Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Add example config service Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Consistent naming for configService Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Docs: Change example repository location Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Fix tests after response field rename Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Revert accidential unrelated change in api hook Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at> * Update server flag descriptions Co-authored-by: Anbraten <anton@ju60.de> Co-authored-by: Anbraten <anton@ju60.de>
202 lines
4.4 KiB
Go
202 lines
4.4 KiB
Go
// httpsignatures is a golang implementation of the http-signatures spec
|
|
// found at https://tools.ietf.org/html/draft-cavage-http-signatures
|
|
package httpsignatures
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
headerSignature = "Signature"
|
|
headerAuthorization = "Authorization"
|
|
|
|
RequestTarget = "(request-target)"
|
|
|
|
authScheme = "Signature "
|
|
)
|
|
|
|
var (
|
|
ErrorNoSignatureHeader = errors.New("No Signature header found in request")
|
|
|
|
signatureRegex = regexp.MustCompile(`(\w+)="([^"]*)"`)
|
|
)
|
|
|
|
// Signature is the hashed key + headers, either from a request or a signer
|
|
type Signature struct {
|
|
KeyID string
|
|
Algorithm *Algorithm
|
|
Headers HeaderList
|
|
Signature string
|
|
}
|
|
|
|
// FromRequest creates a new Signature from the Request
|
|
// both Signature and Authorization http headers are supported.
|
|
func FromRequest(r *http.Request) (*Signature, error) {
|
|
if s, ok := r.Header[headerSignature]; ok {
|
|
return FromString(s[0])
|
|
}
|
|
if a, ok := r.Header[headerAuthorization]; ok {
|
|
return FromString(strings.TrimPrefix(a[0], authScheme))
|
|
}
|
|
return nil, ErrorNoSignatureHeader
|
|
}
|
|
|
|
// FromString creates a new Signature from its encoded form,
|
|
// eg `keyId="a",algorithm="b",headers="c",signature="d"`
|
|
func FromString(in string) (*Signature, error) {
|
|
var res Signature = Signature{}
|
|
var key string
|
|
var value string
|
|
|
|
for _, m := range signatureRegex.FindAllStringSubmatch(in, -1) {
|
|
key = m[1]
|
|
value = m[2]
|
|
|
|
if key == "keyId" {
|
|
res.KeyID = value
|
|
} else if key == "algorithm" {
|
|
alg, err := algorithmFromString(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.Algorithm = alg
|
|
} else if key == "headers" {
|
|
res.Headers = headerListFromString(value)
|
|
} else if key == "signature" {
|
|
res.Signature = value
|
|
} else {
|
|
return nil, errors.New(fmt.Sprintf("Unexpected key in signature '%s'", key))
|
|
}
|
|
}
|
|
|
|
if len(res.Signature) == 0 {
|
|
return nil, errors.New("Missing signature")
|
|
}
|
|
|
|
if len(res.KeyID) == 0 {
|
|
return nil, errors.New("Missing keyId")
|
|
}
|
|
|
|
if res.Algorithm == nil {
|
|
return nil, errors.New("Missing algorithm")
|
|
}
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
// String returns the encoded form of the Signature
|
|
func (s Signature) String() string {
|
|
str := fmt.Sprintf(
|
|
`keyId="%s",algorithm="%s",signature="%s"`,
|
|
s.KeyID,
|
|
s.Algorithm.name,
|
|
s.Signature,
|
|
)
|
|
|
|
if len(s.Headers) > 0 {
|
|
str += fmt.Sprintf(`,headers="%s"`, s.Headers.String())
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
func (s Signature) calculateSignature(key string, r *http.Request) (string, error) {
|
|
hash := hmac.New(s.Algorithm.hash, []byte(key))
|
|
|
|
signingString, err := s.Headers.signingString(r)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
hash.Write([]byte(signingString))
|
|
|
|
return base64.StdEncoding.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// Sign this signature using the given key
|
|
func (s *Signature) sign(key string, r *http.Request) error {
|
|
sig, err := s.calculateSignature(key, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.Signature = sig
|
|
return nil
|
|
}
|
|
|
|
// IsValid validates this signature for the given key
|
|
func (s Signature) IsValid(key string, r *http.Request) bool {
|
|
if !s.Headers.hasDate() {
|
|
return false
|
|
}
|
|
|
|
sig, err := s.calculateSignature(key, r)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return subtle.ConstantTimeCompare([]byte(s.Signature), []byte(sig)) == 1
|
|
}
|
|
|
|
type HeaderList []string
|
|
|
|
func headerListFromString(list string) HeaderList {
|
|
return strings.Split(strings.ToLower(string(list)), " ")
|
|
}
|
|
|
|
func (h HeaderList) String() string {
|
|
return strings.ToLower(strings.Join(h, " "))
|
|
}
|
|
|
|
func (h HeaderList) hasDate() bool {
|
|
for _, header := range h {
|
|
if header == "date" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (h HeaderList) signingString(req *http.Request) (string, error) {
|
|
lines := []string{}
|
|
|
|
for _, header := range h {
|
|
if header == RequestTarget {
|
|
lines = append(lines, requestTargetLine(req))
|
|
} else {
|
|
line, err := headerLine(req, header)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
|
|
return strings.Join(lines, "\n"), nil
|
|
}
|
|
|
|
func requestTargetLine(req *http.Request) string {
|
|
var url string = ""
|
|
if req.URL != nil {
|
|
url = req.URL.RequestURI()
|
|
}
|
|
|
|
return fmt.Sprintf("%s: %s %s", RequestTarget, strings.ToLower(req.Method), url)
|
|
}
|
|
|
|
func headerLine(req *http.Request, header string) (string, error) {
|
|
|
|
if value := req.Header.Get(header); value != "" {
|
|
return fmt.Sprintf("%s: %s", header, value), nil
|
|
}
|
|
|
|
return "", errors.New(fmt.Sprintf("Missing required header '%s'", header))
|
|
}
|