mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 14:31:02 +00:00 
			
		
		
		
	* use certmagic for more extensible/robust ACME cert handling * accept TOS based on config option Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv>
		
			
				
	
	
		
			263 lines
		
	
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			263 lines
		
	
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // Copyright 2020 Matthew Holt
 | |
| //
 | |
| // 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.
 | |
| //
 | |
| // --- ORIGINAL LICENSE ---
 | |
| //
 | |
| // Copyright 2015 The Go Authors. All rights reserved.
 | |
| // Use of this source code is governed by a BSD-style
 | |
| // license that can be found in the THIRD-PARTY file.
 | |
| //
 | |
| // (This file has been modified from its original contents.)
 | |
| // (And it has dragons. Don't wake the dragons.)
 | |
| 
 | |
| package acme
 | |
| 
 | |
| import (
 | |
| 	"crypto"
 | |
| 	"crypto/ecdsa"
 | |
| 	"crypto/hmac"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/rsa"
 | |
| 	"crypto/sha256"
 | |
| 	_ "crypto/sha512" // need for EC keys
 | |
| 	"encoding/base64"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"math/big"
 | |
| )
 | |
| 
 | |
| var errUnsupportedKey = fmt.Errorf("unknown key type; only RSA and ECDSA are supported")
 | |
| 
 | |
| // keyID is the account identity provided by a CA during registration.
 | |
| type keyID string
 | |
| 
 | |
| // noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.
 | |
| // See jwsEncodeJSON for details.
 | |
| const noKeyID = keyID("")
 | |
| 
 | |
| // // noPayload indicates jwsEncodeJSON will encode zero-length octet string
 | |
| // // in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make
 | |
| // // authenticated GET requests via POSTing with an empty payload.
 | |
| // // See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
 | |
| // const noPayload = ""
 | |
| 
 | |
| // jwsEncodeEAB creates a JWS payload for External Account Binding according to RFC 8555 §7.3.4.
 | |
| func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url string) ([]byte, error) {
 | |
| 	// §7.3.4: "The 'alg' field MUST indicate a MAC-based algorithm"
 | |
| 	alg, sha := "HS256", crypto.SHA256
 | |
| 
 | |
| 	// §7.3.4: "The 'nonce' field MUST NOT be present"
 | |
| 	phead, err := jwsHead(alg, "", url, kid, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	encodedKey, err := jwkEncode(accountKey)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	payload := base64.RawURLEncoding.EncodeToString([]byte(encodedKey))
 | |
| 
 | |
| 	payloadToSign := []byte(phead + "." + payload)
 | |
| 
 | |
| 	h := hmac.New(sha256.New, hmacKey)
 | |
| 	h.Write(payloadToSign)
 | |
| 	sig := h.Sum(nil)
 | |
| 
 | |
| 	return jwsFinal(sha, sig, phead, payload)
 | |
| }
 | |
| 
 | |
| // jwsEncodeJSON signs claimset using provided key and a nonce.
 | |
| // The result is serialized in JSON format containing either kid or jwk
 | |
| // fields based on the provided keyID value.
 | |
| //
 | |
| // If kid is non-empty, its quoted value is inserted in the protected head
 | |
| // as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted
 | |
| // as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive.
 | |
| //
 | |
| // See https://tools.ietf.org/html/rfc7515#section-7.
 | |
| //
 | |
| // If nonce is empty, it will not be encoded into the header.
 | |
| func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) {
 | |
| 	alg, sha := jwsHasher(key.Public())
 | |
| 	if alg == "" || !sha.Available() {
 | |
| 		return nil, errUnsupportedKey
 | |
| 	}
 | |
| 
 | |
| 	phead, err := jwsHead(alg, nonce, url, kid, key)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	var payload string
 | |
| 	if claimset != nil {
 | |
| 		cs, err := json.Marshal(claimset)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		payload = base64.RawURLEncoding.EncodeToString(cs)
 | |
| 	}
 | |
| 
 | |
| 	payloadToSign := []byte(phead + "." + payload)
 | |
| 	hash := sha.New()
 | |
| 	_, _ = hash.Write(payloadToSign)
 | |
| 	digest := hash.Sum(nil)
 | |
| 
 | |
| 	sig, err := jwsSign(key, sha, digest)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return jwsFinal(sha, sig, phead, payload)
 | |
| }
 | |
| 
 | |
| // jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
 | |
| // The result is also suitable for creating a JWK thumbprint.
 | |
| // https://tools.ietf.org/html/rfc7517
 | |
| func jwkEncode(pub crypto.PublicKey) (string, error) {
 | |
| 	switch pub := pub.(type) {
 | |
| 	case *rsa.PublicKey:
 | |
| 		// https://tools.ietf.org/html/rfc7518#section-6.3.1
 | |
| 		n := pub.N
 | |
| 		e := big.NewInt(int64(pub.E))
 | |
| 		// Field order is important.
 | |
| 		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
 | |
| 		return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
 | |
| 			base64.RawURLEncoding.EncodeToString(e.Bytes()),
 | |
| 			base64.RawURLEncoding.EncodeToString(n.Bytes()),
 | |
| 		), nil
 | |
| 	case *ecdsa.PublicKey:
 | |
| 		// https://tools.ietf.org/html/rfc7518#section-6.2.1
 | |
| 		p := pub.Curve.Params()
 | |
| 		n := p.BitSize / 8
 | |
| 		if p.BitSize%8 != 0 {
 | |
| 			n++
 | |
| 		}
 | |
| 		x := pub.X.Bytes()
 | |
| 		if n > len(x) {
 | |
| 			x = append(make([]byte, n-len(x)), x...)
 | |
| 		}
 | |
| 		y := pub.Y.Bytes()
 | |
| 		if n > len(y) {
 | |
| 			y = append(make([]byte, n-len(y)), y...)
 | |
| 		}
 | |
| 		// Field order is important.
 | |
| 		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
 | |
| 		return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
 | |
| 			p.Name,
 | |
| 			base64.RawURLEncoding.EncodeToString(x),
 | |
| 			base64.RawURLEncoding.EncodeToString(y),
 | |
| 		), nil
 | |
| 	}
 | |
| 	return "", errUnsupportedKey
 | |
| }
 | |
| 
 | |
| // jwsHead constructs the protected JWS header for the given fields.
 | |
| // Since jwk and kid are mutually-exclusive, the jwk will be encoded
 | |
| // only if kid is empty. If nonce is empty, it will not be encoded.
 | |
| func jwsHead(alg, nonce, url string, kid keyID, key crypto.Signer) (string, error) {
 | |
| 	phead := fmt.Sprintf(`{"alg":%q`, alg)
 | |
| 	if kid == noKeyID {
 | |
| 		jwk, err := jwkEncode(key.Public())
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		phead += fmt.Sprintf(`,"jwk":%s`, jwk)
 | |
| 	} else {
 | |
| 		phead += fmt.Sprintf(`,"kid":%q`, kid)
 | |
| 	}
 | |
| 	if nonce != "" {
 | |
| 		phead += fmt.Sprintf(`,"nonce":%q`, nonce)
 | |
| 	}
 | |
| 	phead += fmt.Sprintf(`,"url":%q}`, url)
 | |
| 	phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
 | |
| 	return phead, nil
 | |
| }
 | |
| 
 | |
| // jwsFinal constructs the final JWS object.
 | |
| func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error) {
 | |
| 	enc := struct {
 | |
| 		Protected string `json:"protected"`
 | |
| 		Payload   string `json:"payload"`
 | |
| 		Sig       string `json:"signature"`
 | |
| 	}{
 | |
| 		Protected: phead,
 | |
| 		Payload:   payload,
 | |
| 		Sig:       base64.RawURLEncoding.EncodeToString(sig),
 | |
| 	}
 | |
| 	result, err := json.Marshal(&enc)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // jwsSign signs the digest using the given key.
 | |
| // The hash is unused for ECDSA keys.
 | |
| //
 | |
| // Note: non-stdlib crypto.Signer implementations are expected to return
 | |
| // the signature in the format as specified in RFC7518.
 | |
| // See https://tools.ietf.org/html/rfc7518 for more details.
 | |
| func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
 | |
| 	if key, ok := key.(*ecdsa.PrivateKey); ok {
 | |
| 		// The key.Sign method of ecdsa returns ASN1-encoded signature.
 | |
| 		// So, we use the package Sign function instead
 | |
| 		// to get R and S values directly and format the result accordingly.
 | |
| 		r, s, err := ecdsa.Sign(rand.Reader, key, digest)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		rb, sb := r.Bytes(), s.Bytes()
 | |
| 		size := key.Params().BitSize / 8
 | |
| 		if size%8 > 0 {
 | |
| 			size++
 | |
| 		}
 | |
| 		sig := make([]byte, size*2)
 | |
| 		copy(sig[size-len(rb):], rb)
 | |
| 		copy(sig[size*2-len(sb):], sb)
 | |
| 		return sig, nil
 | |
| 	}
 | |
| 	return key.Sign(rand.Reader, digest, hash)
 | |
| }
 | |
| 
 | |
| // jwsHasher indicates suitable JWS algorithm name and a hash function
 | |
| // to use for signing a digest with the provided key.
 | |
| // It returns ("", 0) if the key is not supported.
 | |
| func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
 | |
| 	switch pub := pub.(type) {
 | |
| 	case *rsa.PublicKey:
 | |
| 		return "RS256", crypto.SHA256
 | |
| 	case *ecdsa.PublicKey:
 | |
| 		switch pub.Params().Name {
 | |
| 		case "P-256":
 | |
| 			return "ES256", crypto.SHA256
 | |
| 		case "P-384":
 | |
| 			return "ES384", crypto.SHA384
 | |
| 		case "P-521":
 | |
| 			return "ES512", crypto.SHA512
 | |
| 		}
 | |
| 	}
 | |
| 	return "", 0
 | |
| }
 | |
| 
 | |
| // jwkThumbprint creates a JWK thumbprint out of pub
 | |
| // as specified in https://tools.ietf.org/html/rfc7638.
 | |
| func jwkThumbprint(pub crypto.PublicKey) (string, error) {
 | |
| 	jwk, err := jwkEncode(pub)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	b := sha256.Sum256([]byte(jwk))
 | |
| 	return base64.RawURLEncoding.EncodeToString(b[:]), nil
 | |
| }
 |