You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
264 lines
7.9 KiB
264 lines
7.9 KiB
4 years ago
|
// 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 ---
|
||
|
//
|
||
6 years ago
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
4 years ago
|
// 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.)
|
||
6 years ago
|
|
||
|
package acme
|
||
|
|
||
|
import (
|
||
|
"crypto"
|
||
|
"crypto/ecdsa"
|
||
4 years ago
|
"crypto/hmac"
|
||
6 years ago
|
"crypto/rand"
|
||
|
"crypto/rsa"
|
||
|
"crypto/sha256"
|
||
|
_ "crypto/sha512" // need for EC keys
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"math/big"
|
||
|
)
|
||
|
|
||
4 years ago
|
var errUnsupportedKey = fmt.Errorf("unknown key type; only RSA and ECDSA are supported")
|
||
4 years ago
|
|
||
5 years ago
|
// 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("")
|
||
|
|
||
4 years ago
|
// // 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)
|
||
4 years ago
|
}
|
||
|
|
||
6 years ago
|
// jwsEncodeJSON signs claimset using provided key and a nonce.
|
||
5 years ago
|
// 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.
|
||
|
//
|
||
6 years ago
|
// See https://tools.ietf.org/html/rfc7515#section-7.
|
||
4 years ago
|
//
|
||
|
// If nonce is empty, it will not be encoded into the header.
|
||
5 years ago
|
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) {
|
||
6 years ago
|
alg, sha := jwsHasher(key.Public())
|
||
6 years ago
|
if alg == "" || !sha.Available() {
|
||
4 years ago
|
return nil, errUnsupportedKey
|
||
6 years ago
|
}
|
||
4 years ago
|
|
||
|
phead, err := jwsHead(alg, nonce, url, kid, key)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
5 years ago
|
}
|
||
4 years ago
|
|
||
5 years ago
|
var payload string
|
||
4 years ago
|
if claimset != nil {
|
||
5 years ago
|
cs, err := json.Marshal(claimset)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
payload = base64.RawURLEncoding.EncodeToString(cs)
|
||
6 years ago
|
}
|
||
|
|
||
4 years ago
|
payloadToSign := []byte(phead + "." + payload)
|
||
|
hash := sha.New()
|
||
|
_, _ = hash.Write(payloadToSign)
|
||
|
digest := hash.Sum(nil)
|
||
4 years ago
|
|
||
4 years ago
|
sig, err := jwsSign(key, sha, digest)
|
||
4 years ago
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
4 years ago
|
return jwsFinal(sha, sig, phead, payload)
|
||
4 years ago
|
}
|
||
|
|
||
6 years ago
|
// 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
|
||
|
}
|
||
4 years ago
|
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
|
||
6 years ago
|
}
|
||
|
|
||
|
// jwsSign signs the digest using the given key.
|
||
6 years ago
|
// The hash is unused for ECDSA keys.
|
||
4 years ago
|
//
|
||
|
// 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.
|
||
6 years ago
|
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
|
||
4 years ago
|
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)
|
||
6 years ago
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
4 years ago
|
rb, sb := r.Bytes(), s.Bytes()
|
||
|
size := key.Params().BitSize / 8
|
||
6 years ago
|
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
|
||
|
}
|
||
4 years ago
|
return key.Sign(rand.Reader, digest, hash)
|
||
6 years ago
|
}
|
||
|
|
||
|
// 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.
|
||
6 years ago
|
func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
|
||
|
switch pub := pub.(type) {
|
||
|
case *rsa.PublicKey:
|
||
6 years ago
|
return "RS256", crypto.SHA256
|
||
6 years ago
|
case *ecdsa.PublicKey:
|
||
|
switch pub.Params().Name {
|
||
6 years ago
|
case "P-256":
|
||
|
return "ES256", crypto.SHA256
|
||
|
case "P-384":
|
||
|
return "ES384", crypto.SHA384
|
||
|
case "P-521":
|
||
|
return "ES512", crypto.SHA512
|
||
|
}
|
||
|
}
|
||
|
return "", 0
|
||
|
}
|
||
|
|
||
4 years ago
|
// jwkThumbprint creates a JWK thumbprint out of pub
|
||
6 years ago
|
// as specified in https://tools.ietf.org/html/rfc7638.
|
||
4 years ago
|
func jwkThumbprint(pub crypto.PublicKey) (string, error) {
|
||
6 years ago
|
jwk, err := jwkEncode(pub)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
b := sha256.Sum256([]byte(jwk))
|
||
|
return base64.RawURLEncoding.EncodeToString(b[:]), nil
|
||
|
}
|