storage: V4 signature query params and encoding.

Add conformance tests and fix formatting and encoding to
align with standard. Add query parameters to SignedURLOptions.

Fixes #1744
Fixes #1747

Change-Id: Iabd509d7444e47c21f825d4e8dff32fbb2558a9f
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/52070
Reviewed-by: Tyler Bui-Palsulich <tbp@google.com>
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Frank Natividad <franknatividad@google.com>
diff --git a/storage/conformance_test.go b/storage/conformance_test.go
index d102ca6..c2fbaa9 100644
--- a/storage/conformance_test.go
+++ b/storage/conformance_test.go
@@ -73,13 +73,21 @@
 					return time.Unix(tc.Timestamp.Seconds, 0).UTC()
 				}
 
+				qp := url.Values{}
+				if tc.QueryParameters != nil {
+					for k, v := range tc.QueryParameters {
+						qp.Add(k, v)
+					}
+				}
+
 				gotURL, err := SignedURL(tc.Bucket, tc.Object, &SignedURLOptions{
-					GoogleAccessID: googleAccessID,
-					PrivateKey:     []byte(privateKey),
-					Method:         tc.Method,
-					Expires:        utcNow().Add(time.Duration(tc.Expiration) * time.Second),
-					Scheme:         SigningSchemeV4,
-					Headers:        headersAsSlice(tc.Headers),
+					GoogleAccessID:  googleAccessID,
+					PrivateKey:      []byte(privateKey),
+					Method:          tc.Method,
+					Expires:         utcNow().Add(time.Duration(tc.Expiration) * time.Second),
+					Scheme:          SigningSchemeV4,
+					Headers:         headersAsSlice(tc.Headers),
+					QueryParameters: qp,
 				})
 				if err != nil {
 					t.Fatal(err)
diff --git a/storage/internal/test/conformance/test.pb.go b/storage/internal/test/conformance/test.pb.go
index b18dc05..e13d0ca 100644
--- a/storage/internal/test/conformance/test.pb.go
+++ b/storage/internal/test/conformance/test.pb.go
@@ -1,15 +1,13 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // source: test.proto
 
-package storage_v1_tests
+package google_cloud_conformance_storage_v1
 
 import (
 	fmt "fmt"
-
-	proto "github.com/golang/protobuf/proto"
-
 	math "math"
 
+	proto "github.com/golang/protobuf/proto"
 	timestamp "github.com/golang/protobuf/ptypes/timestamp"
 )
 
@@ -22,29 +20,59 @@
 // is compatible with the proto package it is being compiled against.
 // A compilation error at this line likely means your copy of the
 // proto package needs to be updated.
-const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+type SigningV4Test_UrlStyle int32
+
+const (
+	SigningV4Test_PATH_STYLE           SigningV4Test_UrlStyle = 0
+	SigningV4Test_VIRTUAL_HOSTED_STYLE SigningV4Test_UrlStyle = 1
+	SigningV4Test_BUCKET_BOUND_DOMAIN  SigningV4Test_UrlStyle = 2
+)
+
+var SigningV4Test_UrlStyle_name = map[int32]string{
+	0: "PATH_STYLE",
+	1: "VIRTUAL_HOSTED_STYLE",
+	2: "BUCKET_BOUND_DOMAIN",
+}
+
+var SigningV4Test_UrlStyle_value = map[string]int32{
+	"PATH_STYLE":           0,
+	"VIRTUAL_HOSTED_STYLE": 1,
+	"BUCKET_BOUND_DOMAIN":  2,
+}
+
+func (x SigningV4Test_UrlStyle) String() string {
+	return proto.EnumName(SigningV4Test_UrlStyle_name, int32(x))
+}
+
+func (SigningV4Test_UrlStyle) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_c161fcfdc0c3ff1e, []int{1, 0}
+}
 
 type TestFile struct {
-	SigningV4Tests       []*SigningV4Test `protobuf:"bytes,1,rep,name=signing_v4_tests,json=signingV4Tests,proto3" json:"signing_v4_tests,omitempty"`
-	XXX_NoUnkeyedLiteral struct{}         `json:"-"`
-	XXX_unrecognized     []byte           `json:"-"`
-	XXX_sizecache        int32            `json:"-"`
+	SigningV4Tests       []*SigningV4Test    `protobuf:"bytes,1,rep,name=signing_v4_tests,json=signingV4Tests,proto3" json:"signing_v4_tests,omitempty"`
+	PostPolicyV4Tests    []*PostPolicyV4Test `protobuf:"bytes,2,rep,name=post_policy_v4_tests,json=postPolicyV4Tests,proto3" json:"post_policy_v4_tests,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}            `json:"-"`
+	XXX_unrecognized     []byte              `json:"-"`
+	XXX_sizecache        int32               `json:"-"`
 }
 
 func (m *TestFile) Reset()         { *m = TestFile{} }
 func (m *TestFile) String() string { return proto.CompactTextString(m) }
 func (*TestFile) ProtoMessage()    {}
 func (*TestFile) Descriptor() ([]byte, []int) {
-	return fileDescriptor_test_7c0b908fadc4a1ba, []int{0}
+	return fileDescriptor_c161fcfdc0c3ff1e, []int{0}
 }
+
 func (m *TestFile) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_TestFile.Unmarshal(m, b)
 }
 func (m *TestFile) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	return xxx_messageInfo_TestFile.Marshal(b, m, deterministic)
 }
-func (dst *TestFile) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_TestFile.Merge(dst, src)
+func (m *TestFile) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_TestFile.Merge(m, src)
 }
 func (m *TestFile) XXX_Size() int {
 	return xxx_messageInfo_TestFile.Size(m)
@@ -62,35 +90,49 @@
 	return nil
 }
 
+func (m *TestFile) GetPostPolicyV4Tests() []*PostPolicyV4Test {
+	if m != nil {
+		return m.PostPolicyV4Tests
+	}
+	return nil
+}
+
 type SigningV4Test struct {
-	FileName             string               `protobuf:"bytes,1,opt,name=fileName,proto3" json:"fileName,omitempty"`
-	Description          string               `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
-	Bucket               string               `protobuf:"bytes,3,opt,name=bucket,proto3" json:"bucket,omitempty"`
-	Object               string               `protobuf:"bytes,4,opt,name=object,proto3" json:"object,omitempty"`
-	Method               string               `protobuf:"bytes,5,opt,name=method,proto3" json:"method,omitempty"`
-	Expiration           int64                `protobuf:"varint,6,opt,name=expiration,proto3" json:"expiration,omitempty"`
-	Timestamp            *timestamp.Timestamp `protobuf:"bytes,7,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
-	ExpectedUrl          string               `protobuf:"bytes,8,opt,name=expectedUrl,proto3" json:"expectedUrl,omitempty"`
-	Headers              map[string]string    `protobuf:"bytes,9,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
-	XXX_NoUnkeyedLiteral struct{}             `json:"-"`
-	XXX_unrecognized     []byte               `json:"-"`
-	XXX_sizecache        int32                `json:"-"`
+	FileName                 string                 `protobuf:"bytes,1,opt,name=fileName,proto3" json:"fileName,omitempty"`
+	Description              string                 `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
+	Bucket                   string                 `protobuf:"bytes,3,opt,name=bucket,proto3" json:"bucket,omitempty"`
+	Object                   string                 `protobuf:"bytes,4,opt,name=object,proto3" json:"object,omitempty"`
+	Method                   string                 `protobuf:"bytes,5,opt,name=method,proto3" json:"method,omitempty"`
+	Expiration               int64                  `protobuf:"varint,6,opt,name=expiration,proto3" json:"expiration,omitempty"`
+	Timestamp                *timestamp.Timestamp   `protobuf:"bytes,7,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+	ExpectedUrl              string                 `protobuf:"bytes,8,opt,name=expectedUrl,proto3" json:"expectedUrl,omitempty"`
+	Headers                  map[string]string      `protobuf:"bytes,9,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	QueryParameters          map[string]string      `protobuf:"bytes,10,rep,name=query_parameters,json=queryParameters,proto3" json:"query_parameters,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	Scheme                   string                 `protobuf:"bytes,11,opt,name=scheme,proto3" json:"scheme,omitempty"`
+	UrlStyle                 SigningV4Test_UrlStyle `protobuf:"varint,12,opt,name=urlStyle,proto3,enum=google.cloud.conformance.storage.v1.SigningV4Test_UrlStyle" json:"urlStyle,omitempty"`
+	BucketBoundDomain        string                 `protobuf:"bytes,13,opt,name=bucketBoundDomain,proto3" json:"bucketBoundDomain,omitempty"`
+	ExpectedCanonicalRequest string                 `protobuf:"bytes,14,opt,name=expectedCanonicalRequest,proto3" json:"expectedCanonicalRequest,omitempty"`
+	ExpectedStringToSign     string                 `protobuf:"bytes,15,opt,name=expectedStringToSign,proto3" json:"expectedStringToSign,omitempty"`
+	XXX_NoUnkeyedLiteral     struct{}               `json:"-"`
+	XXX_unrecognized         []byte                 `json:"-"`
+	XXX_sizecache            int32                  `json:"-"`
 }
 
 func (m *SigningV4Test) Reset()         { *m = SigningV4Test{} }
 func (m *SigningV4Test) String() string { return proto.CompactTextString(m) }
 func (*SigningV4Test) ProtoMessage()    {}
 func (*SigningV4Test) Descriptor() ([]byte, []int) {
-	return fileDescriptor_test_7c0b908fadc4a1ba, []int{1}
+	return fileDescriptor_c161fcfdc0c3ff1e, []int{1}
 }
+
 func (m *SigningV4Test) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_SigningV4Test.Unmarshal(m, b)
 }
 func (m *SigningV4Test) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	return xxx_messageInfo_SigningV4Test.Marshal(b, m, deterministic)
 }
-func (dst *SigningV4Test) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_SigningV4Test.Merge(dst, src)
+func (m *SigningV4Test) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_SigningV4Test.Merge(m, src)
 }
 func (m *SigningV4Test) XXX_Size() int {
 	return xxx_messageInfo_SigningV4Test.Size(m)
@@ -164,36 +206,422 @@
 	return nil
 }
 
-func init() {
-	proto.RegisterType((*TestFile)(nil), "storage.v1.tests.TestFile")
-	proto.RegisterType((*SigningV4Test)(nil), "storage.v1.tests.SigningV4Test")
-	proto.RegisterMapType((map[string]string)(nil), "storage.v1.tests.SigningV4Test.HeadersEntry")
+func (m *SigningV4Test) GetQueryParameters() map[string]string {
+	if m != nil {
+		return m.QueryParameters
+	}
+	return nil
 }
 
-func init() { proto.RegisterFile("test.proto", fileDescriptor_test_7c0b908fadc4a1ba) }
+func (m *SigningV4Test) GetScheme() string {
+	if m != nil {
+		return m.Scheme
+	}
+	return ""
+}
 
-var fileDescriptor_test_7c0b908fadc4a1ba = []byte{
-	// 337 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x90, 0x4d, 0x4f, 0xf2, 0x40,
-	0x14, 0x85, 0x53, 0xfa, 0xf2, 0x75, 0x79, 0x35, 0x64, 0x62, 0xcc, 0xa4, 0x0b, 0x69, 0x58, 0x75,
-	0x61, 0x86, 0x88, 0x2c, 0x08, 0x7b, 0x89, 0x6e, 0x5c, 0x54, 0x70, 0x4b, 0xfa, 0x71, 0x29, 0x23,
-	0x6d, 0xa7, 0xe9, 0x0c, 0x04, 0x7e, 0x88, 0xff, 0xd7, 0x74, 0x06, 0xb0, 0xba, 0x71, 0x37, 0xe7,
-	0x39, 0xe7, 0xde, 0xc9, 0x3d, 0x00, 0x0a, 0xa5, 0x62, 0x45, 0x29, 0x94, 0x20, 0x7d, 0xa9, 0x44,
-	0x19, 0x24, 0xc8, 0xf6, 0x0f, 0xac, 0xc2, 0xd2, 0x19, 0x24, 0x42, 0x24, 0x29, 0x8e, 0xb4, 0x1f,
-	0xee, 0xd6, 0x23, 0xc5, 0x33, 0x94, 0x2a, 0xc8, 0x0a, 0x33, 0x32, 0x5c, 0x42, 0x67, 0x81, 0x52,
-	0xcd, 0x79, 0x8a, 0xe4, 0x05, 0xfa, 0x92, 0x27, 0x39, 0xcf, 0x93, 0xd5, 0x7e, 0xb2, 0xd2, 0x0b,
-	0xa8, 0xe5, 0xda, 0x5e, 0x6f, 0x3c, 0x60, 0xbf, 0x37, 0xb3, 0x37, 0x93, 0x7c, 0x9f, 0x54, 0xe3,
-	0xfe, 0xb5, 0xac, 0x4b, 0x39, 0xfc, 0xb4, 0xe1, 0xea, 0x47, 0x82, 0x38, 0xd0, 0x59, 0xf3, 0x14,
-	0x5f, 0x83, 0x0c, 0xa9, 0xe5, 0x5a, 0x5e, 0xd7, 0xbf, 0x68, 0xe2, 0x42, 0x2f, 0x46, 0x19, 0x95,
-	0xbc, 0x50, 0x5c, 0xe4, 0xb4, 0xa1, 0xed, 0x3a, 0x22, 0xb7, 0xd0, 0x0a, 0x77, 0xd1, 0x16, 0x15,
-	0xb5, 0xb5, 0x79, 0x52, 0x15, 0x17, 0xe1, 0x07, 0x46, 0x8a, 0xfe, 0x33, 0xdc, 0xa8, 0x8a, 0x67,
-	0xa8, 0x36, 0x22, 0xa6, 0x4d, 0xc3, 0x8d, 0x22, 0x77, 0x00, 0x78, 0x28, 0x78, 0x19, 0xe8, 0x8f,
-	0x5a, 0xae, 0xe5, 0xd9, 0x7e, 0x8d, 0x90, 0x29, 0x74, 0x2f, 0x0d, 0xd1, 0xb6, 0x6b, 0x79, 0xbd,
-	0xb1, 0xc3, 0x4c, 0x87, 0xec, 0xdc, 0x21, 0x5b, 0x9c, 0x13, 0xfe, 0x77, 0xb8, 0xba, 0x01, 0x0f,
-	0x05, 0x46, 0x0a, 0xe3, 0x65, 0x99, 0xd2, 0x8e, 0xb9, 0xa1, 0x86, 0xc8, 0x1c, 0xda, 0x1b, 0x0c,
-	0x62, 0x2c, 0x25, 0xed, 0xea, 0x56, 0xef, 0xff, 0x68, 0x95, 0x3d, 0x9b, 0xf8, 0x53, 0xae, 0xca,
-	0xa3, 0x7f, 0x1e, 0x76, 0x66, 0xf0, 0xbf, 0x6e, 0x90, 0x3e, 0xd8, 0x5b, 0x3c, 0x9e, 0x4a, 0xad,
-	0x9e, 0xe4, 0x06, 0x9a, 0xfb, 0x20, 0xdd, 0xe1, 0xa9, 0x49, 0x23, 0x66, 0x8d, 0xa9, 0x15, 0xb6,
-	0xf4, 0x11, 0x8f, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x36, 0x21, 0xc6, 0x48, 0x36, 0x02, 0x00,
-	0x00,
+func (m *SigningV4Test) GetUrlStyle() SigningV4Test_UrlStyle {
+	if m != nil {
+		return m.UrlStyle
+	}
+	return SigningV4Test_PATH_STYLE
+}
+
+func (m *SigningV4Test) GetBucketBoundDomain() string {
+	if m != nil {
+		return m.BucketBoundDomain
+	}
+	return ""
+}
+
+func (m *SigningV4Test) GetExpectedCanonicalRequest() string {
+	if m != nil {
+		return m.ExpectedCanonicalRequest
+	}
+	return ""
+}
+
+func (m *SigningV4Test) GetExpectedStringToSign() string {
+	if m != nil {
+		return m.ExpectedStringToSign
+	}
+	return ""
+}
+
+type ConditionalMatches struct {
+	Expression           []string `protobuf:"bytes,1,rep,name=expression,proto3" json:"expression,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *ConditionalMatches) Reset()         { *m = ConditionalMatches{} }
+func (m *ConditionalMatches) String() string { return proto.CompactTextString(m) }
+func (*ConditionalMatches) ProtoMessage()    {}
+func (*ConditionalMatches) Descriptor() ([]byte, []int) {
+	return fileDescriptor_c161fcfdc0c3ff1e, []int{2}
+}
+
+func (m *ConditionalMatches) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ConditionalMatches.Unmarshal(m, b)
+}
+func (m *ConditionalMatches) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ConditionalMatches.Marshal(b, m, deterministic)
+}
+func (m *ConditionalMatches) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ConditionalMatches.Merge(m, src)
+}
+func (m *ConditionalMatches) XXX_Size() int {
+	return xxx_messageInfo_ConditionalMatches.Size(m)
+}
+func (m *ConditionalMatches) XXX_DiscardUnknown() {
+	xxx_messageInfo_ConditionalMatches.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ConditionalMatches proto.InternalMessageInfo
+
+func (m *ConditionalMatches) GetExpression() []string {
+	if m != nil {
+		return m.Expression
+	}
+	return nil
+}
+
+type PolicyConditions struct {
+	SuccessActionStatus   string                `protobuf:"bytes,1,opt,name=successActionStatus,proto3" json:"successActionStatus,omitempty"`
+	SuccessActionRedirect string                `protobuf:"bytes,2,opt,name=successActionRedirect,proto3" json:"successActionRedirect,omitempty"`
+	Matches               []*ConditionalMatches `protobuf:"bytes,3,rep,name=matches,proto3" json:"matches,omitempty"`
+	XXX_NoUnkeyedLiteral  struct{}              `json:"-"`
+	XXX_unrecognized      []byte                `json:"-"`
+	XXX_sizecache         int32                 `json:"-"`
+}
+
+func (m *PolicyConditions) Reset()         { *m = PolicyConditions{} }
+func (m *PolicyConditions) String() string { return proto.CompactTextString(m) }
+func (*PolicyConditions) ProtoMessage()    {}
+func (*PolicyConditions) Descriptor() ([]byte, []int) {
+	return fileDescriptor_c161fcfdc0c3ff1e, []int{3}
+}
+
+func (m *PolicyConditions) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_PolicyConditions.Unmarshal(m, b)
+}
+func (m *PolicyConditions) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_PolicyConditions.Marshal(b, m, deterministic)
+}
+func (m *PolicyConditions) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_PolicyConditions.Merge(m, src)
+}
+func (m *PolicyConditions) XXX_Size() int {
+	return xxx_messageInfo_PolicyConditions.Size(m)
+}
+func (m *PolicyConditions) XXX_DiscardUnknown() {
+	xxx_messageInfo_PolicyConditions.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_PolicyConditions proto.InternalMessageInfo
+
+func (m *PolicyConditions) GetSuccessActionStatus() string {
+	if m != nil {
+		return m.SuccessActionStatus
+	}
+	return ""
+}
+
+func (m *PolicyConditions) GetSuccessActionRedirect() string {
+	if m != nil {
+		return m.SuccessActionRedirect
+	}
+	return ""
+}
+
+func (m *PolicyConditions) GetMatches() []*ConditionalMatches {
+	if m != nil {
+		return m.Matches
+	}
+	return nil
+}
+
+type PolicyInput struct {
+	Scheme               string               `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"`
+	Bucket               string               `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"`
+	Object               string               `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
+	Expiration           int64                `protobuf:"varint,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
+	Timestamp            *timestamp.Timestamp `protobuf:"bytes,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+	Headers              map[string]string    `protobuf:"bytes,6,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	Conditions           *PolicyConditions    `protobuf:"bytes,7,opt,name=conditions,proto3" json:"conditions,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}             `json:"-"`
+	XXX_unrecognized     []byte               `json:"-"`
+	XXX_sizecache        int32                `json:"-"`
+}
+
+func (m *PolicyInput) Reset()         { *m = PolicyInput{} }
+func (m *PolicyInput) String() string { return proto.CompactTextString(m) }
+func (*PolicyInput) ProtoMessage()    {}
+func (*PolicyInput) Descriptor() ([]byte, []int) {
+	return fileDescriptor_c161fcfdc0c3ff1e, []int{4}
+}
+
+func (m *PolicyInput) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_PolicyInput.Unmarshal(m, b)
+}
+func (m *PolicyInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_PolicyInput.Marshal(b, m, deterministic)
+}
+func (m *PolicyInput) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_PolicyInput.Merge(m, src)
+}
+func (m *PolicyInput) XXX_Size() int {
+	return xxx_messageInfo_PolicyInput.Size(m)
+}
+func (m *PolicyInput) XXX_DiscardUnknown() {
+	xxx_messageInfo_PolicyInput.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_PolicyInput proto.InternalMessageInfo
+
+func (m *PolicyInput) GetScheme() string {
+	if m != nil {
+		return m.Scheme
+	}
+	return ""
+}
+
+func (m *PolicyInput) GetBucket() string {
+	if m != nil {
+		return m.Bucket
+	}
+	return ""
+}
+
+func (m *PolicyInput) GetObject() string {
+	if m != nil {
+		return m.Object
+	}
+	return ""
+}
+
+func (m *PolicyInput) GetExpiration() int64 {
+	if m != nil {
+		return m.Expiration
+	}
+	return 0
+}
+
+func (m *PolicyInput) GetTimestamp() *timestamp.Timestamp {
+	if m != nil {
+		return m.Timestamp
+	}
+	return nil
+}
+
+func (m *PolicyInput) GetHeaders() map[string]string {
+	if m != nil {
+		return m.Headers
+	}
+	return nil
+}
+
+func (m *PolicyInput) GetConditions() *PolicyConditions {
+	if m != nil {
+		return m.Conditions
+	}
+	return nil
+}
+
+type PolicyOutput struct {
+	Url                   string            `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
+	Key                   string            `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
+	Fields                map[string]string `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	ExpectedDecodedPolicy string            `protobuf:"bytes,4,opt,name=expectedDecodedPolicy,proto3" json:"expectedDecodedPolicy,omitempty"`
+	XXX_NoUnkeyedLiteral  struct{}          `json:"-"`
+	XXX_unrecognized      []byte            `json:"-"`
+	XXX_sizecache         int32             `json:"-"`
+}
+
+func (m *PolicyOutput) Reset()         { *m = PolicyOutput{} }
+func (m *PolicyOutput) String() string { return proto.CompactTextString(m) }
+func (*PolicyOutput) ProtoMessage()    {}
+func (*PolicyOutput) Descriptor() ([]byte, []int) {
+	return fileDescriptor_c161fcfdc0c3ff1e, []int{5}
+}
+
+func (m *PolicyOutput) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_PolicyOutput.Unmarshal(m, b)
+}
+func (m *PolicyOutput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_PolicyOutput.Marshal(b, m, deterministic)
+}
+func (m *PolicyOutput) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_PolicyOutput.Merge(m, src)
+}
+func (m *PolicyOutput) XXX_Size() int {
+	return xxx_messageInfo_PolicyOutput.Size(m)
+}
+func (m *PolicyOutput) XXX_DiscardUnknown() {
+	xxx_messageInfo_PolicyOutput.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_PolicyOutput proto.InternalMessageInfo
+
+func (m *PolicyOutput) GetUrl() string {
+	if m != nil {
+		return m.Url
+	}
+	return ""
+}
+
+func (m *PolicyOutput) GetKey() string {
+	if m != nil {
+		return m.Key
+	}
+	return ""
+}
+
+func (m *PolicyOutput) GetFields() map[string]string {
+	if m != nil {
+		return m.Fields
+	}
+	return nil
+}
+
+func (m *PolicyOutput) GetExpectedDecodedPolicy() string {
+	if m != nil {
+		return m.ExpectedDecodedPolicy
+	}
+	return ""
+}
+
+type PostPolicyV4Test struct {
+	Description          string        `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"`
+	PolicyInput          *PolicyInput  `protobuf:"bytes,2,opt,name=policyInput,proto3" json:"policyInput,omitempty"`
+	PolicyOutput         *PolicyOutput `protobuf:"bytes,3,opt,name=policyOutput,proto3" json:"policyOutput,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}      `json:"-"`
+	XXX_unrecognized     []byte        `json:"-"`
+	XXX_sizecache        int32         `json:"-"`
+}
+
+func (m *PostPolicyV4Test) Reset()         { *m = PostPolicyV4Test{} }
+func (m *PostPolicyV4Test) String() string { return proto.CompactTextString(m) }
+func (*PostPolicyV4Test) ProtoMessage()    {}
+func (*PostPolicyV4Test) Descriptor() ([]byte, []int) {
+	return fileDescriptor_c161fcfdc0c3ff1e, []int{6}
+}
+
+func (m *PostPolicyV4Test) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_PostPolicyV4Test.Unmarshal(m, b)
+}
+func (m *PostPolicyV4Test) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_PostPolicyV4Test.Marshal(b, m, deterministic)
+}
+func (m *PostPolicyV4Test) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_PostPolicyV4Test.Merge(m, src)
+}
+func (m *PostPolicyV4Test) XXX_Size() int {
+	return xxx_messageInfo_PostPolicyV4Test.Size(m)
+}
+func (m *PostPolicyV4Test) XXX_DiscardUnknown() {
+	xxx_messageInfo_PostPolicyV4Test.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_PostPolicyV4Test proto.InternalMessageInfo
+
+func (m *PostPolicyV4Test) GetDescription() string {
+	if m != nil {
+		return m.Description
+	}
+	return ""
+}
+
+func (m *PostPolicyV4Test) GetPolicyInput() *PolicyInput {
+	if m != nil {
+		return m.PolicyInput
+	}
+	return nil
+}
+
+func (m *PostPolicyV4Test) GetPolicyOutput() *PolicyOutput {
+	if m != nil {
+		return m.PolicyOutput
+	}
+	return nil
+}
+
+func init() {
+	proto.RegisterEnum("google.cloud.conformance.storage.v1.SigningV4Test_UrlStyle", SigningV4Test_UrlStyle_name, SigningV4Test_UrlStyle_value)
+	proto.RegisterType((*TestFile)(nil), "google.cloud.conformance.storage.v1.TestFile")
+	proto.RegisterType((*SigningV4Test)(nil), "google.cloud.conformance.storage.v1.SigningV4Test")
+	proto.RegisterMapType((map[string]string)(nil), "google.cloud.conformance.storage.v1.SigningV4Test.HeadersEntry")
+	proto.RegisterMapType((map[string]string)(nil), "google.cloud.conformance.storage.v1.SigningV4Test.QueryParametersEntry")
+	proto.RegisterType((*ConditionalMatches)(nil), "google.cloud.conformance.storage.v1.ConditionalMatches")
+	proto.RegisterType((*PolicyConditions)(nil), "google.cloud.conformance.storage.v1.PolicyConditions")
+	proto.RegisterType((*PolicyInput)(nil), "google.cloud.conformance.storage.v1.PolicyInput")
+	proto.RegisterMapType((map[string]string)(nil), "google.cloud.conformance.storage.v1.PolicyInput.HeadersEntry")
+	proto.RegisterType((*PolicyOutput)(nil), "google.cloud.conformance.storage.v1.PolicyOutput")
+	proto.RegisterMapType((map[string]string)(nil), "google.cloud.conformance.storage.v1.PolicyOutput.FieldsEntry")
+	proto.RegisterType((*PostPolicyV4Test)(nil), "google.cloud.conformance.storage.v1.PostPolicyV4Test")
+}
+
+func init() { proto.RegisterFile("test.proto", fileDescriptor_c161fcfdc0c3ff1e) }
+
+var fileDescriptor_c161fcfdc0c3ff1e = []byte{
+	// 917 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x56, 0xef, 0x6e, 0xda, 0x56,
+	0x14, 0x9f, 0xa1, 0x21, 0xe4, 0x38, 0x4d, 0xe9, 0x2d, 0xdb, 0x2c, 0x3e, 0x6c, 0x88, 0x7d, 0x18,
+	0x93, 0x26, 0xb7, 0x61, 0xa9, 0xd6, 0x65, 0x9a, 0xa6, 0x10, 0xd2, 0x26, 0x5a, 0x93, 0x50, 0x03,
+	0x89, 0x2a, 0x4d, 0x42, 0x8e, 0x7d, 0x00, 0xaf, 0xb6, 0xaf, 0xe3, 0x7b, 0x1d, 0x95, 0x67, 0xd9,
+	0x1b, 0xec, 0x21, 0xf6, 0x06, 0x93, 0xf6, 0x06, 0x7b, 0x8f, 0x7d, 0x9a, 0xee, 0xbd, 0x36, 0x98,
+	0x86, 0x4c, 0x90, 0x7e, 0xe3, 0x9e, 0x73, 0xcf, 0xef, 0xe7, 0xf3, 0xef, 0x77, 0x01, 0xe0, 0xc8,
+	0xb8, 0x19, 0xc5, 0x94, 0x53, 0xf2, 0xd5, 0x98, 0xd2, 0xb1, 0x8f, 0xa6, 0xe3, 0xd3, 0xc4, 0x35,
+	0x1d, 0x1a, 0x8e, 0x68, 0x1c, 0xd8, 0xa1, 0x83, 0x26, 0xe3, 0x34, 0xb6, 0xc7, 0x68, 0xde, 0xec,
+	0xd6, 0xbe, 0x54, 0x97, 0x9e, 0xca, 0x90, 0xab, 0x64, 0xf4, 0x94, 0x7b, 0x01, 0x32, 0x6e, 0x07,
+	0x91, 0x42, 0x69, 0xfc, 0xad, 0x41, 0xb9, 0x8f, 0x8c, 0xbf, 0xf4, 0x7c, 0x24, 0xbf, 0x42, 0x85,
+	0x79, 0xe3, 0xd0, 0x0b, 0xc7, 0xc3, 0x9b, 0xbd, 0xa1, 0xe0, 0x62, 0x86, 0x56, 0x2f, 0x36, 0xf5,
+	0x56, 0xcb, 0x5c, 0x81, 0xcd, 0xec, 0xa9, 0xe0, 0x8b, 0x3d, 0x81, 0x68, 0xed, 0xb0, 0xfc, 0x91,
+	0x91, 0x11, 0x54, 0x23, 0xca, 0xf8, 0x30, 0xa2, 0xbe, 0xe7, 0x4c, 0xe7, 0x0c, 0x05, 0xc9, 0xf0,
+	0x7c, 0x25, 0x86, 0x2e, 0x65, 0xbc, 0x2b, 0xe3, 0x53, 0x92, 0xc7, 0xd1, 0x07, 0x16, 0xd6, 0xf8,
+	0x73, 0x13, 0x1e, 0x2e, 0x7c, 0x09, 0xa9, 0x41, 0x79, 0xe4, 0xf9, 0x78, 0x66, 0x07, 0x68, 0x68,
+	0x75, 0xad, 0xb9, 0x65, 0xcd, 0xce, 0xa4, 0x0e, 0xba, 0x8b, 0xcc, 0x89, 0xbd, 0x88, 0x7b, 0x34,
+	0x34, 0x0a, 0xd2, 0x9d, 0x37, 0x91, 0xcf, 0xa0, 0x74, 0x95, 0x38, 0xef, 0x90, 0x1b, 0x45, 0xe9,
+	0x4c, 0x4f, 0xc2, 0x4e, 0xaf, 0x7e, 0x43, 0x87, 0x1b, 0x0f, 0x94, 0x5d, 0x9d, 0x84, 0x3d, 0x40,
+	0x3e, 0xa1, 0xae, 0xb1, 0xa1, 0xec, 0xea, 0x44, 0xbe, 0x00, 0xc0, 0xf7, 0x91, 0x17, 0xdb, 0x92,
+	0xa8, 0x54, 0xd7, 0x9a, 0x45, 0x2b, 0x67, 0x21, 0x2f, 0x60, 0x6b, 0xd6, 0x1d, 0x63, 0xb3, 0xae,
+	0x35, 0xf5, 0x56, 0x2d, 0x2b, 0x4a, 0xd6, 0x3f, 0xb3, 0x9f, 0xdd, 0xb0, 0xe6, 0x97, 0x45, 0x0e,
+	0xf8, 0x3e, 0x42, 0x87, 0xa3, 0x3b, 0x88, 0x7d, 0xa3, 0xac, 0x72, 0xc8, 0x99, 0xc8, 0x5b, 0xd8,
+	0x9c, 0xa0, 0xed, 0x62, 0xcc, 0x8c, 0x2d, 0x59, 0xee, 0x9f, 0xd7, 0x6f, 0xa8, 0x79, 0xac, 0x10,
+	0x8e, 0x42, 0x1e, 0x4f, 0xad, 0x0c, 0x8f, 0xc4, 0x50, 0xb9, 0x4e, 0x30, 0x9e, 0x0e, 0x23, 0x3b,
+	0xb6, 0x03, 0xe4, 0x82, 0x03, 0x24, 0xc7, 0xab, 0x7b, 0x70, 0xbc, 0x11, 0x50, 0xdd, 0x19, 0x92,
+	0xe2, 0x7a, 0x74, 0xbd, 0x68, 0x15, 0x25, 0x66, 0xce, 0x04, 0x03, 0x34, 0x74, 0x55, 0x62, 0x75,
+	0x22, 0x97, 0x50, 0x4e, 0x62, 0xbf, 0xc7, 0xa7, 0x3e, 0x1a, 0xdb, 0x75, 0xad, 0xb9, 0xd3, 0xfa,
+	0xf1, 0x1e, 0xdf, 0x30, 0x48, 0x21, 0xac, 0x19, 0x18, 0xf9, 0x16, 0x1e, 0xab, 0xae, 0xb7, 0x69,
+	0x12, 0xba, 0x1d, 0x1a, 0xd8, 0x5e, 0x68, 0x3c, 0x94, 0xdc, 0xb7, 0x1d, 0x64, 0x1f, 0x8c, 0xac,
+	0xf8, 0x87, 0x76, 0x48, 0x43, 0xcf, 0xb1, 0x7d, 0x0b, 0xaf, 0x13, 0x64, 0xdc, 0xd8, 0x91, 0x41,
+	0x77, 0xfa, 0x49, 0x0b, 0xaa, 0x99, 0xaf, 0xc7, 0x63, 0x2f, 0x1c, 0xf7, 0xa9, 0xf8, 0x3a, 0xe3,
+	0x91, 0x8c, 0x5b, 0xea, 0xab, 0xed, 0xc3, 0x76, 0xbe, 0x37, 0xa4, 0x02, 0xc5, 0x77, 0x38, 0x4d,
+	0x47, 0x5d, 0xfc, 0x24, 0x55, 0xd8, 0xb8, 0xb1, 0xfd, 0x04, 0xd3, 0xf9, 0x56, 0x87, 0xfd, 0xc2,
+	0x0b, 0xad, 0xd6, 0x86, 0xea, 0xb2, 0x9a, 0xaf, 0x83, 0xd1, 0x38, 0x85, 0x72, 0x56, 0x33, 0xb2,
+	0x03, 0xd0, 0x3d, 0xe8, 0x1f, 0x0f, 0x7b, 0xfd, 0xb7, 0xaf, 0x8f, 0x2a, 0x9f, 0x10, 0x03, 0xaa,
+	0x17, 0x27, 0x56, 0x7f, 0x70, 0xf0, 0x7a, 0x78, 0x7c, 0xde, 0xeb, 0x1f, 0x75, 0x52, 0x8f, 0x46,
+	0x3e, 0x87, 0x27, 0xed, 0xc1, 0xe1, 0x2f, 0x47, 0xfd, 0x61, 0xfb, 0x7c, 0x70, 0xd6, 0x19, 0x76,
+	0xce, 0x4f, 0x0f, 0x4e, 0xce, 0x2a, 0x85, 0xc6, 0x1e, 0x90, 0x43, 0x1a, 0xba, 0x9e, 0xd8, 0x0a,
+	0xdb, 0x3f, 0xb5, 0xb9, 0x33, 0x41, 0x96, 0xae, 0x4f, 0x8c, 0x8c, 0x89, 0xf5, 0x11, 0xb2, 0xb4,
+	0x65, 0xe5, 0x2c, 0x8d, 0xbf, 0x34, 0xa8, 0x28, 0x21, 0x98, 0x05, 0x33, 0xf2, 0x0c, 0x9e, 0xb0,
+	0xc4, 0x71, 0x90, 0xb1, 0x03, 0x47, 0x58, 0x7a, 0xdc, 0xe6, 0x09, 0x4b, 0xb3, 0x5a, 0xe6, 0x22,
+	0x7b, 0xf0, 0xe9, 0x82, 0xd9, 0x42, 0xd7, 0x8b, 0xc5, 0x92, 0xab, 0xac, 0x97, 0x3b, 0xc9, 0x1b,
+	0xd8, 0x0c, 0xd4, 0x77, 0x1a, 0x45, 0x39, 0xfb, 0xdf, 0xaf, 0x34, 0x77, 0xb7, 0xd3, 0xb4, 0x32,
+	0x9c, 0xc6, 0xef, 0x45, 0xd0, 0x55, 0x3e, 0x27, 0x61, 0x94, 0xf0, 0xdc, 0xcc, 0x6b, 0x0b, 0x33,
+	0x3f, 0x97, 0xa7, 0xc2, 0x1d, 0xf2, 0x54, 0x5c, 0x90, 0xa7, 0x45, 0x19, 0x7a, 0xf0, 0xff, 0x32,
+	0xb4, 0xb1, 0x8e, 0x0c, 0x5d, 0xce, 0x45, 0xa6, 0x24, 0x8b, 0xf0, 0xd3, 0x8a, 0x9a, 0x3e, 0x4b,
+	0xf2, 0x0e, 0x89, 0x19, 0x00, 0x38, 0xb3, 0x9e, 0xa6, 0xd2, 0xf8, 0x7c, 0x0d, 0xec, 0xf9, 0x40,
+	0x58, 0x39, 0xa0, 0x8f, 0x59, 0x9b, 0xc6, 0xbf, 0x1a, 0x6c, 0x2b, 0xf0, 0xf3, 0x84, 0x8b, 0xf6,
+	0x54, 0xa0, 0x98, 0xc4, 0x7e, 0x16, 0x9c, 0xc4, 0x7e, 0x06, 0x57, 0x98, 0xc3, 0x0d, 0xa0, 0x34,
+	0xf2, 0xd0, 0x77, 0xb3, 0x21, 0x59, 0xa7, 0x3e, 0x8a, 0xc6, 0x7c, 0x29, 0xe3, 0x55, 0x7d, 0x52,
+	0x30, 0x31, 0xb2, 0x99, 0x2c, 0x74, 0xd0, 0xa1, 0x2e, 0xba, 0x2a, 0x24, 0x7d, 0x97, 0x96, 0x3b,
+	0x6b, 0x3f, 0x80, 0x9e, 0x03, 0x5b, 0x2b, 0xf9, 0x7f, 0xe4, 0xaa, 0x2d, 0xbe, 0xbb, 0x1f, 0x3e,
+	0xa4, 0xda, 0xed, 0x87, 0xd4, 0x02, 0x3d, 0x9a, 0xf7, 0x5a, 0xc2, 0xea, 0xad, 0x67, 0xeb, 0xce,
+	0x88, 0x95, 0x07, 0x21, 0x03, 0xd8, 0x8e, 0x72, 0xf5, 0x91, 0xb3, 0xae, 0xb7, 0x76, 0xd7, 0x2e,
+	0xac, 0xb5, 0x00, 0xd3, 0xbe, 0x84, 0xaf, 0x1d, 0x1a, 0xac, 0x82, 0xd2, 0xd5, 0xfe, 0x28, 0x7c,
+	0xf3, 0x4a, 0xdd, 0x3b, 0x94, 0xf7, 0x7a, 0xa9, 0xef, 0x62, 0xd7, 0x94, 0x7f, 0x48, 0xc4, 0x96,
+	0x67, 0x81, 0x57, 0x25, 0xb9, 0x42, 0xdf, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, 0x0e, 0xac, 0xde,
+	0xde, 0xca, 0x09, 0x00, 0x00,
 }
diff --git a/storage/internal/test/conformance/test.proto b/storage/internal/test/conformance/test.proto
index 517cc81..b08611a 100644
--- a/storage/internal/test/conformance/test.proto
+++ b/storage/internal/test/conformance/test.proto
@@ -14,22 +14,71 @@
 
 syntax = "proto3";
 
-package storage.v1.tests;
+package google.cloud.conformance.storage.v1;
 
 import "google/protobuf/timestamp.proto";
 
+option csharp_namespace = "Google.Cloud.Storage.V1.Tests.Conformance";
+option java_package = "com.google.cloud.conformance.storage.v1";
+option java_multiple_files = true;
+
 message TestFile {
-    repeated SigningV4Test signing_v4_tests = 1;
+  repeated SigningV4Test signing_v4_tests = 1;
+  repeated PostPolicyV4Test post_policy_v4_tests = 2;
 }
 
 message SigningV4Test {
-    string fileName = 1;
-    string description = 2;
-    string bucket = 3;
-    string object = 4;
-    string method = 5;
-    int64 expiration = 6;
-    google.protobuf.Timestamp timestamp = 7;
-    string expectedUrl = 8;
-    map<string, string> headers = 9;
+  string fileName = 1;
+  string description = 2;
+  string bucket = 3;
+  string object = 4;
+  string method = 5;
+  int64 expiration = 6;
+  google.protobuf.Timestamp timestamp = 7;
+  string expectedUrl = 8;
+  map<string, string> headers = 9;
+  map<string, string> query_parameters = 10;
+  string scheme = 11;
+  enum UrlStyle {
+    PATH_STYLE = 0;
+    VIRTUAL_HOSTED_STYLE = 1;
+    BUCKET_BOUND_DOMAIN = 2;
+  }
+  UrlStyle urlStyle = 12;
+  string bucketBoundDomain = 13;
+  string expectedCanonicalRequest = 14;
+  string expectedStringToSign = 15;
+}
+
+message ConditionalMatches {
+  repeated string expression = 1;
+}
+
+message PolicyConditions {
+  string successActionStatus = 1;
+  string successActionRedirect = 2;
+  repeated ConditionalMatches matches = 3;
+}
+
+message PolicyInput {
+  string scheme = 1;
+  string bucket = 2;
+  string object = 3;
+  int64 expiration = 4;
+  google.protobuf.Timestamp timestamp = 5;
+  map<string, string> headers = 6;
+  PolicyConditions conditions = 7;
+}
+
+message PolicyOutput {
+  string url = 1;
+  string key = 2;
+  map<string, string> fields = 3;
+  string expectedDecodedPolicy = 4;
+}
+
+message PostPolicyV4Test {
+  string description = 1;
+  PolicyInput policyInput = 2;
+  PolicyOutput policyOutput = 3;
 }
diff --git a/storage/internal/test/conformance/v4_signatures.json b/storage/internal/test/conformance/v4_signatures.json
index 276510b..62d32a0 100644
--- a/storage/internal/test/conformance/v4_signatures.json
+++ b/storage/internal/test/conformance/v4_signatures.json
@@ -7,7 +7,10 @@
       "method": "GET",
       "expiration": "10",
       "timestamp": "2019-02-01T09:00:00Z",
-      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256\u0026X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request\u0026X-Goog-Date=20190201T090000Z\u0026X-Goog-Expires=10\u0026X-Goog-SignedHeaders=host\u0026X-Goog-Signature=95e6a13d43a1d1962e667f17397f2b80ac9bdd1669210d5e08e0135df9dff4e56113485dbe429ca2266487b9d1796ebdee2d7cf682a6ef3bb9fbb4c351686fba90d7b621cf1c4eb1fdf126460dd25fa0837dfdde0a9fd98662ce60844c458448fb2b352c203d9969cb74efa4bdb742287744a4f2308afa4af0e0773f55e32e92973619249214b97283b2daa14195244444e33f938138d1e5f561088ce8011f4986dda33a556412594db7c12fc40e1ff3f1bedeb7a42f5bcda0b9567f17f65855f65071fabb88ea12371877f3f77f10e1466fff6ff6973b74a933322ff0949ce357e20abe96c3dd5cfab42c9c83e740a4d32b9e11e146f0eb3404d2e975896f74"
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host&X-Goog-Signature=95e6a13d43a1d1962e667f17397f2b80ac9bdd1669210d5e08e0135df9dff4e56113485dbe429ca2266487b9d1796ebdee2d7cf682a6ef3bb9fbb4c351686fba90d7b621cf1c4eb1fdf126460dd25fa0837dfdde0a9fd98662ce60844c458448fb2b352c203d9969cb74efa4bdb742287744a4f2308afa4af0e0773f55e32e92973619249214b97283b2daa14195244444e33f938138d1e5f561088ce8011f4986dda33a556412594db7c12fc40e1ff3f1bedeb7a42f5bcda0b9567f17f65855f65071fabb88ea12371877f3f77f10e1466fff6ff6973b74a933322ff0949ce357e20abe96c3dd5cfab42c9c83e740a4d32b9e11e146f0eb3404d2e975896f74",
+      "scheme": "https",
+      "expectedCanonicalRequest": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n00e2fb794ea93d7adb703edaebdd509821fcc7d4f1a79ac5c8d2b394df109320",
+      "expectedStringToSign": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host\nhost:storage.googleapis.com\n\nhost\nUNSIGNED-PAYLOAD"
     },
     {
       "description": "Simple PUT",
@@ -16,7 +19,10 @@
       "method": "PUT",
       "expiration": "10",
       "timestamp": "2019-02-01T09:00:00Z",
-      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256\u0026X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request\u0026X-Goog-Date=20190201T090000Z\u0026X-Goog-Expires=10\u0026X-Goog-SignedHeaders=host\u0026X-Goog-Signature=8adff1d4285739e31aa68e73767a46bc5511fde377497dbe08481bf5ceb34e29cc9a59921748d8ec3dd4085b7e9b7772a952afedfcdaecb3ae8352275b8b7c867f204e3db85076220a3127a8a9589302fc1181eae13b9b7fe41109ec8cdc93c1e8bac2d7a0cc32a109ca02d06957211326563ab3d3e678a0ba296e298b5fc5e14593c99d444c94724cc4be97015dbff1dca377b508fa0cb7169195de98d0e4ac96c42b918d28c8d92d33e1bd125ce0fb3cd7ad2c45dae65c22628378f6584971b8bf3945b26f2611eb651e9b6a8648970c1ecf386bb71327b082e7296c4e1ee2fc0bdd8983da80af375c817fb1ad491d0bc22c0f51dba0d66e2cffbc90803e47"
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host&X-Goog-Signature=8adff1d4285739e31aa68e73767a46bc5511fde377497dbe08481bf5ceb34e29cc9a59921748d8ec3dd4085b7e9b7772a952afedfcdaecb3ae8352275b8b7c867f204e3db85076220a3127a8a9589302fc1181eae13b9b7fe41109ec8cdc93c1e8bac2d7a0cc32a109ca02d06957211326563ab3d3e678a0ba296e298b5fc5e14593c99d444c94724cc4be97015dbff1dca377b508fa0cb7169195de98d0e4ac96c42b918d28c8d92d33e1bd125ce0fb3cd7ad2c45dae65c22628378f6584971b8bf3945b26f2611eb651e9b6a8648970c1ecf386bb71327b082e7296c4e1ee2fc0bdd8983da80af375c817fb1ad491d0bc22c0f51dba0d66e2cffbc90803e47",
+      "scheme": "https",
+      "expectedCanonicalRequest": "PUT\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host\nhost:storage.googleapis.com\n\nhost\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n78742860705da91404222d5d66ff89850292471199c3c2808d116ad12e6177b4"
     },
     {
       "description": "POST for resumable uploads",
@@ -25,10 +31,13 @@
       "method": "POST",
       "expiration": "10",
       "timestamp": "2019-02-01T09:00:00Z",
-      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256\u0026X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request\u0026X-Goog-Date=20190201T090000Z\u0026X-Goog-Expires=10\u0026X-Goog-SignedHeaders=host%3Bx-goog-resumable\u0026X-Goog-Signature=4a6d39b23343cedf4c30782aed4b384001828c79ffa3a080a481ea01a640dea0a0ceb58d67a12cef3b243c3f036bb3799c6ee88e8db3eaf7d0bdd4b70a228d0736e07eaa1ee076aff5c6ce09dff1f1f03a0d8ead0d2893408dd3604fdabff553aa6d7af2da67cdba6790006a70240f96717b98f1a6ccb24f00940749599be7ef72aaa5358db63ddd54b2de9e2d6d6a586eac4fe25f36d86fc6ab150418e9c6fa01b732cded226c6d62fc95b72473a4cc55a8257482583fe66d9ab6ede909eb41516a8690946c3e87b0f2052eb0e97e012a14b2f721c42e6e19b8a1cd5658ea36264f10b9b1ada66b8ed5bf7ed7d1708377ac6e5fe608ae361fb594d2e5b24c54",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bx-goog-resumable&X-Goog-Signature=4a6d39b23343cedf4c30782aed4b384001828c79ffa3a080a481ea01a640dea0a0ceb58d67a12cef3b243c3f036bb3799c6ee88e8db3eaf7d0bdd4b70a228d0736e07eaa1ee076aff5c6ce09dff1f1f03a0d8ead0d2893408dd3604fdabff553aa6d7af2da67cdba6790006a70240f96717b98f1a6ccb24f00940749599be7ef72aaa5358db63ddd54b2de9e2d6d6a586eac4fe25f36d86fc6ab150418e9c6fa01b732cded226c6d62fc95b72473a4cc55a8257482583fe66d9ab6ede909eb41516a8690946c3e87b0f2052eb0e97e012a14b2f721c42e6e19b8a1cd5658ea36264f10b9b1ada66b8ed5bf7ed7d1708377ac6e5fe608ae361fb594d2e5b24c54",
       "headers": {
-        "X-goog-resumable": "start"
-      }
+        "X-Goog-Resumable": "start"
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "POST\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bx-goog-resumable\nhost:storage.googleapis.com\nx-goog-resumable:start\n\nhost;x-goog-resumable\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n877f8b40179d2753296f2fd6de815ab40503c7a3c446a7b44aa4e74422ff4daf"
     },
     {
       "description": "Vary expiration and timestamp",
@@ -37,7 +46,10 @@
       "method": "GET",
       "expiration": "20",
       "timestamp": "2019-03-01T09:00:00Z",
-      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256\u0026X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190301%2Fauto%2Fstorage%2Fgoog4_request\u0026X-Goog-Date=20190301T090000Z\u0026X-Goog-Expires=20\u0026X-Goog-SignedHeaders=host\u0026X-Goog-Signature=9669ed5b10664dc594c758296580662912cf4bcc5a4ba0b6bf055bcbf6f34eed7bdad664f534962174a924741a0c273a4f67bc1847cef20192a6beab44223bd9d4fbbd749c407b79997598c30f82ddc269ff47ec09fa3afe74e00616d438df0d96a7d8ad0adacfad1dc3286f864d924fe919fb0dce45d3d975c5afe8e13af2db9cc37ba77835f92f7669b61e94c6d562196c1274529e76cfff1564cc2cad7d5387dc8e12f7a5dfd925685fe92c30b43709eee29fa2f66067472cee5423d1a3a4182fe8cea75c9329d181dc6acad7c393cd04f8bf5bc0515127d8ebd65d80c08e19ad03316053ea60033fd1b1fd85a69c576415da3bf0a3718d9ea6d03e0d66f0"
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190301%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190301T090000Z&X-Goog-Expires=20&X-Goog-SignedHeaders=host&X-Goog-Signature=9669ed5b10664dc594c758296580662912cf4bcc5a4ba0b6bf055bcbf6f34eed7bdad664f534962174a924741a0c273a4f67bc1847cef20192a6beab44223bd9d4fbbd749c407b79997598c30f82ddc269ff47ec09fa3afe74e00616d438df0d96a7d8ad0adacfad1dc3286f864d924fe919fb0dce45d3d975c5afe8e13af2db9cc37ba77835f92f7669b61e94c6d562196c1274529e76cfff1564cc2cad7d5387dc8e12f7a5dfd925685fe92c30b43709eee29fa2f66067472cee5423d1a3a4182fe8cea75c9329d181dc6acad7c393cd04f8bf5bc0515127d8ebd65d80c08e19ad03316053ea60033fd1b1fd85a69c576415da3bf0a3718d9ea6d03e0d66f0",
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190301%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190301T090000Z&X-Goog-Expires=20&X-Goog-SignedHeaders=host\nhost:storage.googleapis.com\nhost\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190301T090000Z\n20190301/auto/storage/goog4_request\n779f19fdb6fd381390e2d5af04947cf21750277ee3c20e0c97b7e46a1dff8907"
     },
     {
       "description": "Vary bucket and object",
@@ -46,7 +58,37 @@
       "method": "GET",
       "expiration": "10",
       "timestamp": "2019-02-01T09:00:00Z",
-      "expectedUrl": "https://storage.googleapis.com/test-bucket2/test-object2?X-Goog-Algorithm=GOOG4-RSA-SHA256\u0026X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request\u0026X-Goog-Date=20190201T090000Z\u0026X-Goog-Expires=10\u0026X-Goog-SignedHeaders=host\u0026X-Goog-Signature=36e3d58dfd3ec1d2dd2f24b5ee372a71e811ffaa2162a2b871d26728d0354270bc116face87127532969c4a3967ed05b7309af741e19c7202f3167aa8c2ac420b61417d6451442bb91d7c822cd17be8783f01e05372769c88913561d27e6660dd8259f0081a71f831be6c50283626cbf04494ac10c394b29bb3bce74ab91548f58a37118a452693cf0483d77561fc9cac8f1765d2c724994cca46a83517a10157ee0347a233a2aaeae6e6ab5e204ff8fc5f54f90a3efdb8301d9fff5475d58cd05b181affd657f48203f4fb133c3a3d355b8eefbd10d5a0a5fd70d06e9515460ad74e22334b2cba4b29cae4f6f285cdb92d8f3126d7a1479ca3bdb69c207d860"
+      "expectedUrl": "https://storage.googleapis.com/test-bucket2/test-object2?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host&X-Goog-Signature=36e3d58dfd3ec1d2dd2f24b5ee372a71e811ffaa2162a2b871d26728d0354270bc116face87127532969c4a3967ed05b7309af741e19c7202f3167aa8c2ac420b61417d6451442bb91d7c822cd17be8783f01e05372769c88913561d27e6660dd8259f0081a71f831be6c50283626cbf04494ac10c394b29bb3bce74ab91548f58a37118a452693cf0483d77561fc9cac8f1765d2c724994cca46a83517a10157ee0347a233a2aaeae6e6ab5e204ff8fc5f54f90a3efdb8301d9fff5475d58cd05b181affd657f48203f4fb133c3a3d355b8eefbd10d5a0a5fd70d06e9515460ad74e22334b2cba4b29cae4f6f285cdb92d8f3126d7a1479ca3bdb69c207d860",
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket2/test-object2\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host\nhost:storage.googleapis.com\n\nhost\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\na139afbf35ac30e9864f63197f79609731ab1b0ca166e2a456dba156fcd3f9ce"
+    },
+    {
+      "description": "Slashes in object name should not be URL encoded",
+      "bucket": "test-bucket",
+      "object": "path/with/slashes/under_score/amper&sand/file.ext",
+      "headers": {
+        "header/name/with/slash": "should-be-encoded"
+      },
+      "method": "GET",
+      "expiration": "10",
+      "timestamp": "2019-02-01T09:00:00Z",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/path/with/slashes/under_score/amper%26sand/file.ext?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=header%2Fname%2Fwith%2Fslash%3Bhost&X-Goog-Signature=2a9a82e84e39f5d2c0d980514db17f8c3dece473c9a5743d54e8453f9811927b1b99ce548c534cababd8fa339183e75b410e12e32a4c72f5ff176e95651fabed0072e59e7e236eb7e26f52c0ce599db1c47ae07af1a98d20872b6fde23432c0a5fcf4fb2dda735169198c80cd5cc51be9904f7e5eef2cc489ff44ac5697c529e4b34ac08709a7d2e425619377212c64561ed8b4d2fcb70a26e4f9236f995ab4658d240ac85c7a353bae6b2d39d5fc0716afa435a1f6e100db5504612b5e610db370623ab4b8eba3c03c98f23dcb4b9ffd518f2212abb2f93649d25385d71603d470cff0b7631adb9d0849d38609dedb3097761c8f47ec0d57777bb063611c05b",
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/path/with/slashes/under_score/amper%26sand/file.ext\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=header%2Fname%2Fwith%2Fslash%3Bhost\nheader/name/with/slash:should-be-encoded\nhost:storage.googleapis.com\n\nheader/name/with/slash;host\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\nf1d206dd8cbe1b892d4081ccddae0927d9f5fee5653fb2a2f43e7c20ed455cad"
+    },
+    {
+      "description": "Forward Slashes should not be stripped",
+      "bucket": "test-bucket",
+      "object": "/path/with/slashes/under_score/amper&sand/file.ext",
+      "method": "GET",
+      "expiration": "10",
+      "timestamp": "2019-02-01T09:00:00Z",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket//path/with/slashes/under_score/amper%26sand/file.ext?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host&X-Goog-Signature=2db8b70e3f85b39be7824f6d02be31af2e6a2eb63f6bb41254851f7ef51bdad8963a9d2b254f8379c1780c8e6898be002d4100a0abd3d45f1437687fed65d15dd237c3a6f3c399c64ffd4e4cea7ef1c2f0391d35ecbeeaf3e3148d23c6f24c839cfcd92c1496332f5bfbbf1ed1e957eb45fad57df24828c96cf243eec23fba014d277c22a572708beb355888c5a8c0047cb3015d7f62cc90285676e7e34626fd0ce9ba5e0da39fc3de0035cc3ad120c46cb73db87246ae123f7a342c235e9480bd7d7e00c13b1e1bb7be5e2bce74d59a53505172463b48aefeedb48281d90874aa4177c881d3596ed1067f02eaac13d810a7aed234c41978b1394d0ce3662f76",
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket//path/with/slashes/under_score/amper%26sand/file.ext\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host\nhost:storage.googleapis.com\n\nhost\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n63c601ecd6ccfec84f1113fc906609cbdf7651395f4300cecd96ddd2c35164f8"
     },
     {
       "description": "Simple headers",
@@ -55,11 +97,14 @@
       "method": "GET",
       "expiration": "10",
       "timestamp": "2019-02-01T09:00:00Z",
-      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256\u0026X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request\u0026X-Goog-Date=20190201T090000Z\u0026X-Goog-Expires=10\u0026X-Goog-SignedHeaders=bar%3Bfoo%3Bhost\u0026X-Goog-Signature=68ecd3b008328ed30d91e2fe37444ed7b9b03f28ed4424555b5161980531ef87db1c3a5bc0265aad5640af30f96014c94fb2dba7479c41bfe1c020eb90c0c6d387d4dd09d4a5df8b60ea50eb6b01cdd786a1e37020f5f95eb8f9b6cd3f65a1f8a8a65c9fcb61ea662959efd9cd73b683f8d8804ef4d6d9b2852419b013368842731359d7f9e6d1139032ceca75d5e67cee5fd0192ea2125e5f2955d38d3d50cf116f3a52e6a62de77f6207f5b95aaa1d7d0f8a46de89ea72e7ea30f21286318d7eba0142232b0deb3a1dc9e1e812a981c66b5ffda3c6b01a8a9d113155792309fd53a3acfd054ca7776e8eec28c26480cd1e3c812f67f91d14217f39a606669d",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=bar%3Bfoo%3Bhost&X-Goog-Signature=68ecd3b008328ed30d91e2fe37444ed7b9b03f28ed4424555b5161980531ef87db1c3a5bc0265aad5640af30f96014c94fb2dba7479c41bfe1c020eb90c0c6d387d4dd09d4a5df8b60ea50eb6b01cdd786a1e37020f5f95eb8f9b6cd3f65a1f8a8a65c9fcb61ea662959efd9cd73b683f8d8804ef4d6d9b2852419b013368842731359d7f9e6d1139032ceca75d5e67cee5fd0192ea2125e5f2955d38d3d50cf116f3a52e6a62de77f6207f5b95aaa1d7d0f8a46de89ea72e7ea30f21286318d7eba0142232b0deb3a1dc9e1e812a981c66b5ffda3c6b01a8a9d113155792309fd53a3acfd054ca7776e8eec28c26480cd1e3c812f67f91d14217f39a606669d",
       "headers": {
         "BAR": "BAR-value",
         "foo": "foo-value"
-      }
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=bar%3Bfoo%3Bhost\nbar:BAR-value\nfoo:foo-value\nhost:storage.googleapis.com\n\nbar;foo;host\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n59c1ac1a6ee7d773d5c4487ecc861d60b71c4871dd18fc7d8485fac09df1d296"
     },
     {
       "description": "Headers should be trimmed",
@@ -68,13 +113,16 @@
       "method": "GET",
       "expiration": "10",
       "timestamp": "2019-02-01T09:00:00Z",
-      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=collapsed%3Bhost%3Bleading%3Btabs%3Btrailing&X-Goog-Signature=52fa1d70bcd527ee9c1241f87c56bc481526e8a63d440948595ff776faacb0caa6e8a3060b113546cb27ed29d80c88d402947d83948758d4e5c49e47d9482751d46b2a99c2dae5bc8f7baffab03dec05b28b5d605610686c48e867d6a4239a2a61a785df7d6099d155bba57d0d331d66d667b5df8e165e8277e2675678fc28499abd34053a2bc4e4fa21d032c4278fd29897e8307f142506a3d8d07149cded15f7defa77028fb88ff45132cee5f6232feb8e7f899fe361f1f8ceed0795aff860084f35e27475447dc6e64e4baa09e96a725eee6fa3c408d6bb51c2bd5f649afb8339f46997d9ef22496a79cf0846e52ac941c08dc4b9d63639d0ff2ce8637412",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=collapsed%3Bhost%3Bleading%3Btabs%3Btrailing&X-Goog-Signature=75d77a3ed2d9b74ff7e1e23b2fd7cc714ad4cc32518c65f3a8197827cd87d302623bab990cf2ff3a633bfaae69b6c2d897add78c105aa68411229610421c4239579add4aff6bdbd5067a0fd61c3aa0029d7de0f8ae88fa3458fa70f875e841d6df9598597d9012b9f848c6857e08f2704ca2f332c71738490ffdda2ed928f9340549d7295745725062d28dc1696eab7cb3b88ac4fd445e951423f645d680a60dd8033d65b65f4c10286f59f4258dbb2bcf36a76ffdd40574104cbbf0b76901c24df5854f24c42e9192fcedc386d85704fec6a6bad3a5201e1fb6c491a4c43371b0913420743580daf3504e99204c6ec894b4d70cd27bc60c3fe2850e8bf3ed22",
       "headers": {
         "collapsed": "abc    def",
         "leading": "    xyz",
         "trailing": "abc    ",
         "tabs": "\tabc\t\t\t\tdef\t"
-      }
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=collapsed%3Bhost%3Bleading%3Btabs%3Btrailing\ncollapsed:abc def\nhost:storage.googleapis.com\nleading:xyz\ntabs:abc def\ntrailing:abc\n\ncollapsed;host;leading;tabs;trailing\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n19153e83555808dbfeb8969043cc8ce8d5db0cce91dc11fb9df58b8130f09d42"
     },
     {
       "description": "Header value with multiple inline values",
@@ -85,8 +133,11 @@
       "timestamp": "2019-02-01T09:00:00Z",
       "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bmultiple&X-Goog-Signature=5cc113735625341f59c7203f0c2c9febc95ba6af6b9c38814f8e523214712087dc0996e4960d273ae1889f248ac1e58d4d19cb3a69ad7670e9a8ca1b434e878f59339dc7006cf32dfd715337e9f593e0504371839174962a08294586e0c78160a7aa303397888c8350637c6af3b32ac310886cc4590bfda9ca561ee58fb5b8ec56bc606d2ada6e7df31f4276e9dcb96bcaea39dc2cd096f3fad774f9c4b30e317ad43736c05f76831437f44e8726c1e90d3f6c9827dc273f211f32fc85658dfc5d357eb606743a6b00a29e519eef1bebaf9db3e8f4b1f5f9afb648ad06e60bc42fa8b57025056697c874c9ea76f5a73201c9717ea43e54713ff3502ff3fc626b",
       "headers": {
-        "multiple": " xyz ,  abc, def  , xyz   "
-      }
+          "multiple": " xyz ,  abc, def  , xyz   "
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bmultiple\nhost:storage.googleapis.com\nmultiple:xyz , abc, def , xyz\n\nhost;multiple\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n4df8e486146c31f1c8cd4e4c730554cde4326791ba48ec11fa969a3de064cd7f"
     },
     {
       "description": "Customer-supplied encryption key",
@@ -100,7 +151,10 @@
         "X-Goog-Encryption-Algorithm": "AES256",
         "X-Goog-Encryption-Key": "key",
         "X-Goog-Encryption-Key-Sha256": "key-hash"
-      }
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bx-goog-encryption-algorithm%3Bx-goog-encryption-key%3Bx-goog-encryption-key-sha256\nhost:storage.googleapis.com\nx-goog-encryption-algorithm:AES256\nx-goog-encryption-key:key\nx-goog-encryption-key-sha256:key-hash\n\nhost;x-goog-encryption-algorithm;x-goog-encryption-key;x-goog-encryption-key-sha256\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n66a45104eba8bdd9748723b45cbd54c3f0f6dba337a5deb9fb6a66334223dc06"
     },
     {
       "description": "List Objects",
@@ -108,7 +162,72 @@
       "method": "GET",
       "expiration": "10",
       "timestamp": "2019-02-01T09:00:00Z",
-      "expectedUrl": "https://storage.googleapis.com/test-bucket?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host&X-Goog-Signature=6dbe94f8e52b2b8a9a476b1c857efa474e09944e2b52b925800316e094a7169d8dbe0df9c0ac08dabb22ac7e827470ceccd65f5a3eadba2a4fb9beebfe37f0d9bb1e552b851fa31a25045bdf019e507f5feb44f061551ef1aeb18dcec0e38ba2e2f77d560a46eaace9c56ed9aa642281301a9d848b0eb30749e34bc7f73a3d596240533466ff9b5f289cd0d4c845c7d96b82a35a5abd0c3aff83e4440ee6873e796087f43545544dc8c01afe1d79c726696b6f555371e491980e7ec145cca0803cf562c38f3fa1d724242f5dea25aac91d74ec9ddd739ff65523627763eaef25cd1f95ad985aaf0079b7c74eb5bcb2870a9b137a7b2c8e41fbe838c95872f75b"
+      "expectedUrl": "https://storage.googleapis.com/test-bucket?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host&X-Goog-Signature=6dbe94f8e52b2b8a9a476b1c857efa474e09944e2b52b925800316e094a7169d8dbe0df9c0ac08dabb22ac7e827470ceccd65f5a3eadba2a4fb9beebfe37f0d9bb1e552b851fa31a25045bdf019e507f5feb44f061551ef1aeb18dcec0e38ba2e2f77d560a46eaace9c56ed9aa642281301a9d848b0eb30749e34bc7f73a3d596240533466ff9b5f289cd0d4c845c7d96b82a35a5abd0c3aff83e4440ee6873e796087f43545544dc8c01afe1d79c726696b6f555371e491980e7ec145cca0803cf562c38f3fa1d724242f5dea25aac91d74ec9ddd739ff65523627763eaef25cd1f95ad985aaf0079b7c74eb5bcb2870a9b137a7b2c8e41fbe838c95872f75b",
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host\nhost:storage.googleapis.com\n\nhost\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n51a7426c2a6c6ab80f336855fc629461ff182fb1d2cb552ac68e5ce8e25db487"
+    },
+    {
+      "description": "Query Parameter Encoding",
+      "bucket": "test-bucket",
+      "object": "test-object",
+      "method": "GET",
+      "expiration": "10",
+      "timestamp": "2019-02-01T09:00:00Z",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host&aA0%C3%A9%2F%3D%25-_.~=~._-%25%3D%2F%C3%A90Aa&X-Goog-Signature=221f1905382ce560042b0441e678b6589f4a661fd319f079bde1f7d7ab07e8515334dbabd901c95d3f16a03f389f661ef7de897fdc7cea8914d93ac8638ad56a9dca62ec8983478a9513a702e12dd57182b5b5ee58d7e94dd685f6c2bbaec1ad168294eaf8300cafec7565e1ad99f55b324caa48720d541e1b2be39b10baa7ff39d2cb77efdad91d63fa0c80625234430027077f68f8ad8c258ef8aba93e2a15fb3f74111e9ffab46f481899d1e83db7d84d9b2645975086ba67ce2d9284d50bb2725871d05621a791ee1c9db7db8a52d579191c5f59da6063128effbe0bbc1ae9a573e298e63aa29bbe9bb8dba76a6c98154a9f03f5ce0cb8f176e0ad14acae",
+      "queryParameters": {
+        "aA0é/=%-_.~": "~._-%=/é0Aa"
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host&aA0%C3%A9%2F%3D%25-_.~=~._-%25%3D%2F%C3%A90Aa\nhost:storage.googleapis.com\n\nhost\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\na4815f31e2df44febcde5f15614a38dfaab1d2dbc2488c92e0dfaef06351448a"
+    },
+    {
+      "description": "Query Parameter Ordering",
+      "bucket": "test-bucket",
+      "object": "test-object",
+      "method": "GET",
+      "expiration": "10",
+      "timestamp": "2019-02-01T09:00:00Z",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-Meta-Foo=bar&X-Goog-SignedHeaders=host&prefix=%2Ffoo&X-Goog-Signature=a07745e1b3d59b85cbe11aa766df72c22468959e7217615dccb7f030234f66b60b37e480f30725ed51f29816362ca8286c619ebb66448ff1d370be2a4a48aacf20d3d2d6200ed17341a5791baf2ee5cd9c2823adacc6264f66c8a54fa887e1bce3c55cf78fb2f6a52618cf09d6f945f63d148052a7b66a75e075ff5065828a806b84bdc49a42399be7483225c720d5e18a6160f79d815f433e7921694fe1d041099851793c2581db0e5ca503cfb566e414f900ceede5f9b22030edd32ab20b6f7f9fb2afba89098b9364e03397c03a94eac3a140c99979b8786844fb4f6c62c1985378939dd1bbaea8e41b9100dda85a27733171cc78d96ee362ea2c3432f4d8",
+      "queryParameters": {
+        "prefix": "/foo",
+        "X-Goog-Meta-Foo": "bar"
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-Meta-Foo=bar&X-Goog-SignedHeaders=host&prefix=%2Ffoo\nhost:storage.googleapis.com\n\nhost\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n4dafe74ad142f32b7c25fc4e6b38fd3b8a6339d7f112247573fb0066f637db6c"
+    },
+    {
+      "description": "Header Ordering",
+      "bucket": "test-bucket",
+      "object": "test-object",
+      "method": "GET",
+      "expiration": "10",
+      "timestamp": "2019-02-01T09:00:00Z",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bx-goog-date&X-Goog-Signature=55a28435997457f1498291e878fd39c5f321973057d2541886020fdfd212b1467d9eeffdc70951ea952d634cb4193e657ed5b7860c46d37f7d904774680a16e518aa9dff273e8441d6893de615eb592e3113d682ad64a87eb0e0c48df17c30f899e7f940ba230530b30f725ab9ec38789682413752de6a026ae69dd858843100645f3ec986aed618d229f8844d378e0e66e907ede6dff7aac56723f51eb830e8877a56100c86a876173424602abefe6c22b6540a2b36634860b2e89137f297cca8f080bdf3433a9d614c5ab2ec84f65412b45516b30500886a2300f23c3423ae0e91546e3471ee08d06894bddc76203a418d46f35bf0b4574f7b24a693fb046c",
+      "headers": {
+        "X-Goog-Date": "20190201T090000Z"
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "GET\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bx-goog-date\nhost:storage.googleapis.com\nx-goog-date:20190201T090000Z\n\nhost;x-goog-date\nUNSIGNED-PAYLOAD",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\n4052143280d90d5f4a8c878ff7418be6fee5d34e50b1da28d8081a094b88fa61"
+    },
+    {
+      "description": "Signed Payload Instead of UNSIGNED-PAYLOAD",
+      "bucket": "test-bucket",
+      "object": "test-object",
+      "method": "PUT",
+      "expiration": "10",
+      "timestamp": "2019-02-01T09:00:00Z",
+      "expectedUrl": "https://storage.googleapis.com/test-bucket/test-object?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bx-goog-content-sha256%3Bx-testcasemetadata-payload-value&X-Goog-Signature=3e5a9669e9aa162888dff1553d24c159bad4f16d444987f6a1b26d8ad0cb7927f15bfaf79c205324d2138fd1f62edb255430c77a03c0d6e9601399e2519014f9e1a7051d9be735cde530022c84602b1c4c25c86cb1e1584489e49d511c9a618a1a8443af31626ca5b2ad105eda1e4499f52b4043f3c1a3bd40c06c0cae36bb19a50ed8671e5d2cdbb148a196ce5a8c14d6970c08225da293e1ef400c92e7a3d5ba0a29ad0893827c96b203a04b04ebd51929bf99b323beba93097dfee700ee2c1bd97013779e5c8f156e56175d4d07e453b2eb0d616086f9f4753dde63507efe88b0dec29c872d25d9465f07778b16b532814148c578ee7e64ed8437006fa551",
+      "headers": {
+        "X-Goog-Content-SHA256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b982",
+        "X-TestCaseMetadata-Payload-Value": "hello"
+      },
+      "scheme": "https",
+      "expectedCanonicalRequest": "PUT\n/test-bucket/test-object\nX-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test-iam-credentials%40dummy-project-id.iam.gserviceaccount.com%2F20190201%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20190201T090000Z&X-Goog-Expires=10&X-Goog-SignedHeaders=host%3Bx-goog-content-sha256%3Bx-testcasemetadata-payload-value\nhost:storage.googleapis.com\nx-goog-content-sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b982\nx-testcasemetadata-payload-value:hello\n\nhost;x-goog-content-sha256;x-testcasemetadata-payload-value\n2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b982",
+      "expectedStringToSign": "GOOG4-RSA-SHA256\n20190201T090000Z\n20190201/auto/storage/goog4_request\nbe21a0841a897930ff5cf72e6e74ec5274efd76c3fe4cde6678f24a0a3d6dbec"
     }
   ]
 }
diff --git a/storage/storage.go b/storage/storage.go
index 9d9e92f..382b175 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -226,10 +226,18 @@
 	ContentType string
 
 	// Headers is a list of extension headers the client must provide
-	// in order to use the generated signed URL.
+	// in order to use the generated signed URL. Each must be a string of the
+	// form "key:values", with multiple values separated by a semicolon.
 	// Optional.
 	Headers []string
 
+	// QueryParameters is a map of additional query parameters. When
+	// SigningScheme is V4, this is used in computing the signature, and the
+	// client must use the same query parameters when using the generated signed
+	// URL.
+	// Optional.
+	QueryParameters url.Values
+
 	// MD5 is the base64 encoded MD5 checksum of the file.
 	// If provided, the client should provide the exact value on the request
 	// header in order to use the signed URL.
@@ -431,6 +439,21 @@
 	return res
 }
 
+// pathEncodeV4 creates an encoded string that matches the v4 signature spec.
+// Following the spec precisely is necessary in order to ensure that the URL
+// and signing string are correctly formed, and Go's url.PathEncode and
+// url.QueryEncode don't generate an exact match without some additional logic.
+func pathEncodeV4(path string) string {
+	segments := strings.Split(path, "/")
+	var encodedSegments []string
+	for _, s := range segments {
+		encodedSegments = append(encodedSegments, url.QueryEscape(s))
+	}
+	encodedStr := strings.Join(encodedSegments, "/")
+	encodedStr = strings.Replace(encodedStr, "+", "%20", -1)
+	return encodedStr
+}
+
 // signedURLV4 creates a signed URL using the sigV4 algorithm.
 func signedURLV4(bucket, name string, opts *SignedURLOptions, now time.Time) (string, error) {
 	buf := &bytes.Buffer{}
@@ -439,11 +462,12 @@
 	if name != "" {
 		u.Path += "/" + name
 	}
+	u.RawPath = pathEncodeV4(u.Path)
 
 	// Note: we have to add a / here because GCS does so auto-magically, despite
-	// Go's EscapedPath not doing so (and we have to exactly match their
+	// our encoding not doing so (and we have to exactly match their
 	// canonical query).
-	fmt.Fprintf(buf, "/%s\n", u.EscapedPath())
+	fmt.Fprintf(buf, "/%s\n", u.RawPath)
 
 	headerNames := append(extractHeaderNames(opts.Headers), "host")
 	if opts.ContentType != "" {
@@ -463,6 +487,12 @@
 		"X-Goog-Expires":       {fmt.Sprintf("%d", int(opts.Expires.Sub(now).Seconds()))},
 		"X-Goog-SignedHeaders": {signedHeaders},
 	}
+	// Add user-supplied query parameters to the canonical query string. For V4,
+	// it's necessary to include these.
+	for k, v := range opts.QueryParameters {
+		canonicalQueryString[k] = append(canonicalQueryString[k], v...)
+	}
+
 	fmt.Fprintf(buf, "%s\n", canonicalQueryString.Encode())
 
 	u.Host = "storage.googleapis.com"
@@ -471,15 +501,33 @@
 	headersWithValue = append(headersWithValue, "host:"+u.Host)
 	headersWithValue = append(headersWithValue, opts.Headers...)
 	if opts.ContentType != "" {
-		headersWithValue = append(headersWithValue, "content-type:"+strings.TrimSpace(opts.ContentType))
+		headersWithValue = append(headersWithValue, "content-type:"+opts.ContentType)
 	}
 	if opts.MD5 != "" {
-		headersWithValue = append(headersWithValue, "content-md5:"+strings.TrimSpace(opts.MD5))
+		headersWithValue = append(headersWithValue, "content-md5:"+opts.MD5)
 	}
-	canonicalHeaders := strings.Join(sortHeadersByKey(headersWithValue), "\n")
+	// Trim extra whitespace from headers and replace with a single space.
+	var trimmedHeaders []string
+	for _, h := range headersWithValue {
+		trimmedHeaders = append(trimmedHeaders, strings.Join(strings.Fields(h), " "))
+	}
+	canonicalHeaders := strings.Join(sortHeadersByKey(trimmedHeaders), "\n")
 	fmt.Fprintf(buf, "%s\n\n", canonicalHeaders)
 	fmt.Fprintf(buf, "%s\n", signedHeaders)
-	fmt.Fprint(buf, "UNSIGNED-PAYLOAD")
+
+	// If the user provides a value for X-Goog-Content-SHA256, we must use
+	// that value in the request string. If not, we use UNSIGNED-PAYLOAD.
+	sha256Header := false
+	for _, h := range trimmedHeaders {
+		if strings.HasPrefix(strings.ToLower(h), "x-goog-content-sha256") && strings.Contains(h, ":") {
+			sha256Header = true
+			fmt.Fprintf(buf, "%s", strings.SplitN(h, ":", 2)[1])
+			break
+		}
+	}
+	if !sha256Header {
+		fmt.Fprint(buf, "UNSIGNED-PAYLOAD")
+	}
 
 	sum := sha256.Sum256(buf.Bytes())
 	hexDigest := hex.EncodeToString(sum[:])
diff --git a/storage/storage_test.go b/storage/storage_test.go
index 2b38965..2bf13c6 100644
--- a/storage/storage_test.go
+++ b/storage/storage_test.go
@@ -487,6 +487,31 @@
 	}
 }
 
+func TestPathEncodeV4(t *testing.T) {
+	tests := []struct {
+		input string
+		want  string
+	}{
+		{
+			"path/with/slashes",
+			"path/with/slashes",
+		},
+		{
+			"path/with/speci@lchar$&",
+			"path/with/speci%40lchar%24%26",
+		},
+		{
+			"path/with/un_ersc_re/~tilde/sp  ace/",
+			"path/with/un_ersc_re/~tilde/sp%20%20ace/",
+		},
+	}
+	for _, test := range tests {
+		if got := pathEncodeV4(test.input); got != test.want {
+			t.Errorf("pathEncodeV4(%q) =  %q, want %q", test.input, got, test.want)
+		}
+	}
+}
+
 func dummyKey(kind string) []byte {
 	slurp, err := ioutil.ReadFile(fmt.Sprintf("./internal/test/dummy_%s", kind))
 	if err != nil {