src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

dpop.go (3552B)


      1 package logincreds
      2 
      3 import (
      4 	"context"
      5 	"crypto/ecdsa"
      6 	cryptorand "crypto/rand"
      7 	"crypto/sha256"
      8 	"crypto/x509"
      9 	"encoding/base64"
     10 	"encoding/json"
     11 	"encoding/pem"
     12 	"fmt"
     13 
     14 	"github.com/aws/aws-sdk-go-v2/internal/sdk"
     15 	"github.com/aws/aws-sdk-go-v2/service/signin"
     16 	"github.com/aws/smithy-go/middleware"
     17 	smithyrand "github.com/aws/smithy-go/rand"
     18 	smithyhttp "github.com/aws/smithy-go/transport/http"
     19 )
     20 
     21 // AWS signin DPOP always uses the P256 curve
     22 const curvelen = 256 / 8 // bytes
     23 
     24 // https://datatracker.ietf.org/doc/html/rfc9449
     25 func mkdpop(token *loginToken, htu string) (string, error) {
     26 	key, err := parseKey(token.DPOPKey)
     27 	if err != nil {
     28 		return "", fmt.Errorf("parse key: %w", err)
     29 	}
     30 
     31 	header, err := jsonb64(&dpopHeader{
     32 		Typ: "dpop+jwt",
     33 		Alg: "ES256",
     34 		Jwk: &dpopHeaderJwk{
     35 			Kty: "EC",
     36 			X:   base64.RawURLEncoding.EncodeToString(key.X.Bytes()),
     37 			Y:   base64.RawURLEncoding.EncodeToString(key.Y.Bytes()),
     38 			Crv: "P-256",
     39 		},
     40 	})
     41 	if err != nil {
     42 		return "", fmt.Errorf("marshal header: %w", err)
     43 	}
     44 
     45 	uuid, err := smithyrand.NewUUID(cryptorand.Reader).GetUUID()
     46 	if err != nil {
     47 		return "", fmt.Errorf("uuid: %w", err)
     48 	}
     49 
     50 	payload, err := jsonb64(&dpopPayload{
     51 		Jti: uuid,
     52 		Htm: "POST",
     53 		Htu: htu,
     54 		Iat: sdk.NowTime().Unix(),
     55 	})
     56 	if err != nil {
     57 		return "", fmt.Errorf("marshal payload: %w", err)
     58 	}
     59 
     60 	msg := fmt.Sprintf("%s.%s", header, payload)
     61 
     62 	h := sha256.New()
     63 	h.Write([]byte(msg))
     64 
     65 	r, s, err := ecdsa.Sign(cryptorand.Reader, key, h.Sum(nil))
     66 	if err != nil {
     67 		return "", fmt.Errorf("sign: %w", err)
     68 	}
     69 
     70 	// DPOP signatures are formatted in RAW r || s form (with each value padded
     71 	// to fit in curve size which in our case is always the 256 bits) - rather
     72 	// than encoded in something like asn.1
     73 	sig := make([]byte, curvelen*2)
     74 	r.FillBytes(sig[0:curvelen])
     75 	s.FillBytes(sig[curvelen:])
     76 
     77 	dpop := fmt.Sprintf("%s.%s", msg, base64.RawURLEncoding.EncodeToString(sig))
     78 	return dpop, nil
     79 }
     80 
     81 func parseKey(pemBlock string) (*ecdsa.PrivateKey, error) {
     82 	block, _ := pem.Decode([]byte(pemBlock))
     83 	priv, err := x509.ParseECPrivateKey(block.Bytes)
     84 	if err != nil {
     85 		return nil, fmt.Errorf("parse ec private key: %w", err)
     86 	}
     87 
     88 	return priv, nil
     89 }
     90 
     91 func jsonb64(v any) (string, error) {
     92 	j, err := json.MarshalIndent(v, "", "  ")
     93 	if err != nil {
     94 		return "", err
     95 	}
     96 
     97 	return base64.RawURLEncoding.EncodeToString(j), nil
     98 }
     99 
    100 type dpopHeader struct {
    101 	Typ string         `json:"typ"`
    102 	Alg string         `json:"alg"`
    103 	Jwk *dpopHeaderJwk `json:"jwk"`
    104 }
    105 
    106 type dpopHeaderJwk struct {
    107 	Kty string `json:"kty"`
    108 	X   string `json:"x"`
    109 	Y   string `json:"y"`
    110 	Crv string `json:"crv"`
    111 }
    112 
    113 type dpopPayload struct {
    114 	Jti string `json:"jti"`
    115 	Htm string `json:"htm"`
    116 	Htu string `json:"htu"`
    117 	Iat int64  `json:"iat"`
    118 }
    119 
    120 type signDPOP struct {
    121 	Token *loginToken
    122 }
    123 
    124 func addSignDPOP(token *loginToken) func(o *signin.Options) {
    125 	return signin.WithAPIOptions(func(stack *middleware.Stack) error {
    126 		return stack.Finalize.Add(&signDPOP{token}, middleware.After)
    127 	})
    128 }
    129 
    130 func (*signDPOP) ID() string {
    131 	return "signDPOP"
    132 }
    133 
    134 func (m *signDPOP) HandleFinalize(
    135 	ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (
    136 	out middleware.FinalizeOutput, md middleware.Metadata, err error,
    137 ) {
    138 	req, ok := in.Request.(*smithyhttp.Request)
    139 	if !ok {
    140 		return out, md, fmt.Errorf("unexpected transport type %T", req)
    141 	}
    142 
    143 	dpop, err := mkdpop(m.Token, req.URL.String())
    144 	if err != nil {
    145 		return out, md, fmt.Errorf("sign dpop: %w", err)
    146 	}
    147 
    148 	req.Header.Set("DPoP", dpop)
    149 	return next.HandleFinalize(ctx, in)
    150 }