mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-05-31 22:58:35 +00:00
cc30db44ac
* use async key pair for webhooks * fix tests * fix linter * improve code * add key pair to database * undo some changes * more undo * improve docs * add api-endpoint * add signaturne api endpoint * fix error * fix linting and test * fix lint * add test * migration 006 * no need for migration * replace httsign lib * fix lint Co-authored-by: 6543 <6543@obermui.de>
250 lines
6.7 KiB
Go
250 lines
6.7 KiB
Go
// Copyright (C) 2017 Space Monkey, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package httpsig
|
|
|
|
import (
|
|
"crypto"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Verifier is used by by the HTTP server to verify the incoming HTTP requests
|
|
type Verifier struct {
|
|
keyGetter KeyGetter
|
|
requiredHeaders []string
|
|
}
|
|
|
|
// NewVerifier creates a new Verifier using kg to get the key
|
|
// mapped to the ID received in the requests
|
|
func NewVerifier(kg KeyGetter) *Verifier {
|
|
v := &Verifier{
|
|
keyGetter: kg,
|
|
}
|
|
v.SetRequiredHeaders(nil)
|
|
return v
|
|
}
|
|
|
|
// RequiredHeaders returns the required header the client have to include in
|
|
// the signature
|
|
func (v *Verifier) RequiredHeaders() []string {
|
|
return append([]string{}, v.requiredHeaders...)
|
|
}
|
|
|
|
// SetRequiredHeaders set the list of headers to be included by the client to generate the signature
|
|
func (v *Verifier) SetRequiredHeaders(headers []string) {
|
|
if len(headers) == 0 {
|
|
headers = []string{"date"}
|
|
}
|
|
requiredHeaders := make([]string, 0, len(headers))
|
|
for _, h := range headers {
|
|
requiredHeaders = append(requiredHeaders, strings.ToLower(h))
|
|
}
|
|
v.requiredHeaders = requiredHeaders
|
|
}
|
|
|
|
// Verify parses req and verify the signature using the key returned by
|
|
// the keyGetter. It returns the KeyId parameter from he signature header
|
|
// and a nil error if the signature verifies, an error otherwise
|
|
func (v *Verifier) Verify(req *http.Request) (string, error) {
|
|
// retrieve and validate params from the request
|
|
params := getParamsFromAuthHeader(req)
|
|
if params == nil {
|
|
return "", fmt.Errorf("no params present")
|
|
}
|
|
if params.KeyID == "" {
|
|
return "", fmt.Errorf("keyId is required")
|
|
}
|
|
if params.Algorithm == "" {
|
|
return "", fmt.Errorf("algorithm is required")
|
|
}
|
|
if len(params.Signature) == 0 {
|
|
return "", fmt.Errorf("signature is required")
|
|
}
|
|
if len(params.Headers) == 0 {
|
|
params.Headers = []string{"date"}
|
|
}
|
|
|
|
header_check:
|
|
for _, h := range v.requiredHeaders {
|
|
for _, header := range params.Headers {
|
|
if strings.EqualFold(h, header) {
|
|
continue header_check
|
|
}
|
|
}
|
|
return "", fmt.Errorf("missing required header in signature %q",
|
|
h)
|
|
}
|
|
|
|
// calculate signature string for request
|
|
sigData, err := BuildSignatureData(req, params.Headers, params.Created, params.Expires)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// look up key based on keyId
|
|
key, err := v.keyGetter.GetKey(params.KeyID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// we still leave this sanity check
|
|
if key == nil {
|
|
return "", fmt.Errorf("no key with id %q", params.KeyID)
|
|
}
|
|
|
|
switch params.Algorithm {
|
|
case "rsa-sha1":
|
|
rsaPubkey := toRSAPublicKey(key)
|
|
if rsaPubkey == nil {
|
|
return "", fmt.Errorf("algorithm %q is not supported by key %q",
|
|
params.Algorithm, params.KeyID)
|
|
}
|
|
return params.KeyID, RSAVerify(rsaPubkey, crypto.SHA1, sigData, params.Signature)
|
|
case "rsa-sha256":
|
|
rsaPubkey := toRSAPublicKey(key)
|
|
if rsaPubkey == nil {
|
|
return "", fmt.Errorf("algorithm %q is not supported by key %q",
|
|
params.Algorithm, params.KeyID)
|
|
}
|
|
return params.KeyID, RSAVerify(rsaPubkey, crypto.SHA256, sigData, params.Signature)
|
|
case "hmac-sha256":
|
|
hmacKey := toHMACKey(key)
|
|
if hmacKey == nil {
|
|
return "", fmt.Errorf("algorithm %q is not supported by key %q",
|
|
params.Algorithm, params.KeyID)
|
|
}
|
|
return params.KeyID, HMACVerify(hmacKey, crypto.SHA256, sigData, params.Signature)
|
|
case "ed25519":
|
|
ed25519Key := toEd25519PublicKey(key)
|
|
if ed25519Key == nil {
|
|
return "", fmt.Errorf("algorithm %q is not supported by key %q",
|
|
params.Algorithm, params.KeyID)
|
|
}
|
|
return params.KeyID, Ed25519Verify(ed25519Key, sigData, params.Signature)
|
|
default:
|
|
return "", fmt.Errorf("unsupported algorithm %q", params.Algorithm)
|
|
}
|
|
}
|
|
|
|
// paramRE scans out recognized parameter keypairs. accepted values are those
|
|
// that are quoted
|
|
var paramRE = regexp.MustCompile(`(?U)\s*([a-zA-Z][a-zA-Z0-9_]*)\s*=\s*"(.*)"\s*`)
|
|
|
|
// Params holds the field requires to build the signature string
|
|
type Params struct {
|
|
KeyID string
|
|
Algorithm string
|
|
Headers []string
|
|
Signature []byte
|
|
Created time.Time
|
|
Expires time.Time
|
|
}
|
|
|
|
func getParamsFromAuthHeader(req *http.Request) *Params {
|
|
return getParams(req, "Authorization", "Signature ")
|
|
}
|
|
|
|
func getParams(req *http.Request, header, prefix string) *Params {
|
|
values := req.Header[http.CanonicalHeaderKey(header)]
|
|
// last well-formed parameter wins
|
|
for i := len(values) - 1; i >= 0; i-- {
|
|
value := values[i]
|
|
if prefix != "" {
|
|
if trimmed := strings.TrimPrefix(value, prefix); trimmed != value {
|
|
value = trimmed
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
matches := paramRE.FindAllStringSubmatch(value, -1)
|
|
if matches == nil {
|
|
continue
|
|
}
|
|
|
|
params := Params{}
|
|
// malformed parameters get ignored.
|
|
for _, match := range matches {
|
|
switch match[1] {
|
|
case "keyId":
|
|
params.KeyID = match[2]
|
|
case "algorithm":
|
|
if algorithm, ok := parseAlgorithm(match[2]); ok {
|
|
params.Algorithm = algorithm
|
|
}
|
|
case "headers":
|
|
if headers, ok := parseHeaders(match[2]); ok {
|
|
params.Headers = headers
|
|
}
|
|
case "signature":
|
|
if signature, ok := parseSignature(match[2]); ok {
|
|
params.Signature = signature
|
|
}
|
|
case "created":
|
|
if created, ok := parseTime(match[2]); ok {
|
|
params.Created = created
|
|
}
|
|
case "expires":
|
|
if expires, ok := parseTime(match[2]); ok {
|
|
params.Expires = expires
|
|
}
|
|
}
|
|
}
|
|
return ¶ms
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseAlgorithm parses recognized algorithm values
|
|
func parseAlgorithm(s string) (algorithm string, ok bool) {
|
|
s = strings.TrimSpace(s)
|
|
switch s {
|
|
case "rsa-sha1", "rsa-sha256", "hmac-sha256", "ed25519":
|
|
return s, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// parseHeaders parses a space separated list of header values.
|
|
func parseHeaders(s string) (headers []string, ok bool) {
|
|
for _, header := range strings.Split(s, " ") {
|
|
if header != "" {
|
|
headers = append(headers, strings.ToLower(header))
|
|
}
|
|
}
|
|
return headers, true
|
|
}
|
|
|
|
func parseSignature(s string) (signature []byte, ok bool) {
|
|
signature, err := base64.StdEncoding.DecodeString(s)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return signature, true
|
|
}
|
|
|
|
func parseTime(s string) (t time.Time, ok bool) {
|
|
sec, err := strconv.ParseInt(s, 10, 64)
|
|
if err != nil {
|
|
return t, false
|
|
}
|
|
return time.Unix(sec, 0), true
|
|
}
|