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 }