diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 7d799c88..ed43ef0e 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -24,12 +24,14 @@ import ( ) var ( - defaultDisableRenewal = false - globalProvisionerClaims = provisioner.Claims{ - MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, - MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, - DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, - DisableRenewal: &defaultDisableRenewal, + defaultDisableRenewal = false + defaultDisableSmallstepExtensions = false + globalProvisionerClaims = provisioner.Claims{ + MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, + MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, + DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, + DisableRenewal: &defaultDisableRenewal, + DisableSmallstepExtensions: &defaultDisableSmallstepExtensions, } ) diff --git a/acme/api/order.go b/acme/api/order.go index b207f87c..14549e75 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/json" + "fmt" "net" "net/http" "strings" @@ -16,6 +17,7 @@ import ( "go.step.sm/crypto/x509util" "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/acme/wire" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" @@ -48,16 +50,86 @@ func (n *NewOrderRequest) Validate() error { if id.Value == "" { return acme.NewError(acme.ErrorMalformedType, "permanent identifier cannot be empty") } + case acme.WireUser, acme.WireDevice: + // validation of Wire identifiers is performed in `validateWireIdentifiers`, but + // marked here as known and supported types. + continue default: return acme.NewError(acme.ErrorMalformedType, "identifier type unsupported: %s", id.Type) } + } - // TODO(hs): add some validations for DNS domains? - // TODO(hs): combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1 + if err := n.validateWireIdentifiers(); err != nil { + return acme.WrapError(acme.ErrorMalformedType, err, "failed validating Wire identifiers") } + + // TODO(hs): add some validations for DNS domains? + // TODO(hs): combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1 + return nil } +func (n *NewOrderRequest) validateWireIdentifiers() error { + if !n.hasWireIdentifiers() { + return nil + } + + userIdentifiers := identifiersOfType(acme.WireUser, n.Identifiers) + deviceIdentifiers := identifiersOfType(acme.WireDevice, n.Identifiers) + + if len(userIdentifiers) != 1 { + return fmt.Errorf("expected exactly one Wire UserID identifier; got %d", len(userIdentifiers)) + } + if len(deviceIdentifiers) != 1 { + return fmt.Errorf("expected exactly one Wire DeviceID identifier, got %d", len(deviceIdentifiers)) + } + + wireUserID, err := wire.ParseUserID(userIdentifiers[0].Value) + if err != nil { + return fmt.Errorf("failed parsing Wire UserID: %w", err) + } + + wireDeviceID, err := wire.ParseDeviceID(deviceIdentifiers[0].Value) + if err != nil { + return fmt.Errorf("failed parsing Wire DeviceID: %w", err) + } + if _, err := wire.ParseClientID(wireDeviceID.ClientID); err != nil { + return fmt.Errorf("invalid Wire client ID %q: %w", wireDeviceID.ClientID, err) + } + + switch { + case wireUserID.Domain != wireDeviceID.Domain: + return fmt.Errorf("UserID domain %q does not match DeviceID domain %q", wireUserID.Domain, wireDeviceID.Domain) + case wireUserID.Name != wireDeviceID.Name: + return fmt.Errorf("UserID name %q does not match DeviceID name %q", wireUserID.Name, wireDeviceID.Name) + case wireUserID.Handle != wireDeviceID.Handle: + return fmt.Errorf("UserID handle %q does not match DeviceID handle %q", wireUserID.Handle, wireDeviceID.Handle) + } + + return nil +} + +// hasWireIdentifiers returns whether the [NewOrderRequest] contains +// Wire identifiers. +func (n *NewOrderRequest) hasWireIdentifiers() bool { + for _, i := range n.Identifiers { + if i.Type == acme.WireUser || i.Type == acme.WireDevice { + return true + } + } + return false +} + +// identifiersOfType returns the Identifiers that are of type typ. +func identifiersOfType(typ acme.IdentifierType, ids []acme.Identifier) (result []acme.Identifier) { + for _, id := range ids { + if id.Type == typ { + result = append(result, id) + } + } + return +} + // FinalizeRequest captures the body for a Finalize order request. type FinalizeRequest struct { CSR string `json:"csr"` @@ -262,12 +334,43 @@ func newAuthorization(ctx context.Context, az *acme.Authorization) error { continue } + var target string + switch az.Identifier.Type { + case acme.WireUser: + wireOptions, err := prov.GetOptions().GetWireOptions() + if err != nil { + return acme.WrapErrorISE(err, "failed getting Wire options") + } + target, err = wireOptions.GetOIDCOptions().EvaluateTarget("") // TODO(hs): determine if required by Wire + if err != nil { + return acme.WrapError(acme.ErrorMalformedType, err, "invalid Go template registered for 'target'") + } + case acme.WireDevice: + wireID, err := wire.ParseDeviceID(az.Identifier.Value) + if err != nil { + return acme.WrapError(acme.ErrorMalformedType, err, "failed parsing WireDevice") + } + clientID, err := wire.ParseClientID(wireID.ClientID) + if err != nil { + return acme.WrapError(acme.ErrorMalformedType, err, "failed parsing ClientID") + } + wireOptions, err := prov.GetOptions().GetWireOptions() + if err != nil { + return acme.WrapErrorISE(err, "failed getting Wire options") + } + target, err = wireOptions.GetDPOPOptions().EvaluateTarget(clientID.DeviceID) + if err != nil { + return acme.WrapError(acme.ErrorMalformedType, err, "invalid Go template registered for 'target'") + } + } + ch := &acme.Challenge{ AccountID: az.AccountID, Value: az.Identifier.Value, Type: typ, Token: az.Token, Status: acme.StatusPending, + Target: target, } if err := db.CreateChallenge(ctx, ch); err != nil { return acme.WrapErrorISE(err, "error creating challenge") @@ -399,6 +502,10 @@ func challengeTypes(az *acme.Authorization) []acme.ChallengeType { } case acme.PermanentIdentifier: chTypes = []acme.ChallengeType{acme.DEVICEATTEST01} + case acme.WireUser: + chTypes = []acme.ChallengeType{acme.WIREOIDC01} + case acme.WireDevice: + chTypes = []acme.ChallengeType{acme.WIREDPOP01} default: chTypes = []acme.ChallengeType{} } diff --git a/acme/api/order_test.go b/acme/api/order_test.go index 36de975a..2552bbb0 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -24,6 +24,10 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/authority/provisioner/wire" + + sassert "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewOrderRequest_Validate(t *testing.T) { @@ -80,7 +84,7 @@ func TestNewOrderRequest_Validate(t *testing.T) { err: acme.NewError(acme.ErrorMalformedType, "invalid DNS name: *.example.com:8080"), } }, - "fail/bad-ip": func(t *testing.T) test { + "fail/bad-identifier/ip": func(t *testing.T) test { nbf := time.Now().UTC().Add(time.Minute) naf := time.Now().UTC().Add(5 * time.Minute) return test{ @@ -96,6 +100,39 @@ func TestNewOrderRequest_Validate(t *testing.T) { err: acme.NewError(acme.ErrorMalformedType, "invalid IP address: %s", "192.168.42.1000"), } }, + "fail/bad-identifier/wireapp-invalid-uri": func(t *testing.T) test { + return test{ + nor: &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "wireapp-user", Value: `{"name": "Alice Smith", "domain": "wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`}, + {Type: "wireapp-device", Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`}, + }, + }, + err: acme.NewError(acme.ErrorMalformedType, `failed validating Wire identifiers: invalid Wire client ID "example.com": invalid Wire client ID scheme ""; expected "wireapp"`), + } + }, + "fail/bad-identifier/wireapp-wrong-scheme": func(t *testing.T) test { + return test{ + nor: &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "wireapp-user", Value: `{"name": "Alice Smith", "domain": "wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`}, + {Type: "wireapp-device", Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "nowireapp://example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`}, + }, + }, + err: acme.NewError(acme.ErrorMalformedType, `failed validating Wire identifiers: invalid Wire client ID "nowireapp://example.com": invalid Wire client ID scheme "nowireapp"; expected "wireapp"`), + } + }, + "fail/bad-identifier/wireapp-invalid-user-parts": func(t *testing.T) test { + return test{ + nor: &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "wireapp-user", Value: `{"name": "Alice Smith", "domain": "wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`}, + {Type: "wireapp-device", Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "wireapp://user-device@example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`}, + }, + }, + err: acme.NewError(acme.ErrorMalformedType, `failed validating Wire identifiers: invalid Wire client ID "wireapp://user-device@example.com": invalid Wire client ID username "user-device"`), + } + }, "ok": func(t *testing.T) test { nbf := time.Now().UTC().Add(time.Minute) naf := time.Now().UTC().Add(5 * time.Minute) @@ -174,34 +211,50 @@ func TestNewOrderRequest_Validate(t *testing.T) { naf: naf, } }, + "ok/wireapp": func(t *testing.T) test { + nbf := time.Now().UTC().Add(time.Minute) + naf := time.Now().UTC().Add(5 * time.Minute) + return test{ + nor: &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "wireapp-user", Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`}, + {Type: "wireapp-device", Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`}, + }, + NotAfter: naf, + NotBefore: nbf, + }, + nbf: nbf, + naf: naf, + } + }, } for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - if err := tc.nor.Validate(); err != nil { - if assert.NotNil(t, err) { - var ae *acme.Error - if assert.True(t, errors.As(err, &ae)) { - assert.HasPrefix(t, ae.Error(), tc.err.Error()) - assert.Equals(t, ae.StatusCode(), tc.err.StatusCode()) - assert.Equals(t, ae.Type, tc.err.Type) - } + err := tc.nor.Validate() + if tc.err != nil { + assert.Error(t, err) + var ae *acme.Error + if assert.True(t, errors.As(err, &ae)) { + assert.HasPrefix(t, ae.Error(), tc.err.Error()) + assert.Equals(t, ae.StatusCode(), tc.err.StatusCode()) + assert.Equals(t, ae.Type, tc.err.Type) } + return + } + + assert.NoError(t, err) + if tc.nbf.IsZero() { + assert.True(t, tc.nor.NotBefore.Before(time.Now().Add(time.Minute))) + assert.True(t, tc.nor.NotBefore.After(time.Now().Add(-time.Minute))) } else { - if assert.Nil(t, tc.err) { - if tc.nbf.IsZero() { - assert.True(t, tc.nor.NotBefore.Before(time.Now().Add(time.Minute))) - assert.True(t, tc.nor.NotBefore.After(time.Now().Add(-time.Minute))) - } else { - assert.Equals(t, tc.nor.NotBefore, tc.nbf) - } - if tc.naf.IsZero() { - assert.True(t, tc.nor.NotAfter.Before(time.Now().Add(24*time.Hour))) - assert.True(t, tc.nor.NotAfter.After(time.Now().Add(24*time.Hour-time.Minute))) - } else { - assert.Equals(t, tc.nor.NotAfter, tc.naf) - } - } + assert.Equals(t, tc.nor.NotBefore, tc.nbf) + } + if tc.naf.IsZero() { + assert.True(t, tc.nor.NotAfter.Before(time.Now().Add(24*time.Hour))) + assert.True(t, tc.nor.NotAfter.After(time.Now().Add(24*time.Hour-time.Minute))) + } else { + assert.Equals(t, tc.nor.NotAfter, tc.naf) } }) } @@ -503,6 +556,37 @@ func TestHandler_GetOrder(t *testing.T) { func TestHandler_newAuthorization(t *testing.T) { defaultProvisioner := newProv() + fakeKey := `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----` + wireProvisioner := newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wire.Options{ + OIDC: &wire.OIDCOptions{ + Provider: &wire.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wire.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wire.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + }) + wireProvisionerFailOptions := &provisioner.ACME{ + Type: "ACME", + Name: "test@acme-provisioner.com", + Options: &provisioner.Options{}, + Challenges: []provisioner.ACMEChallenge{ + provisioner.WIREOIDC_01, + provisioner.WIREDPOP_01, + }, + } type test struct { az *acme.Authorization prov acme.Provisioner @@ -530,8 +614,13 @@ func TestHandler_newAuthorization(t *testing.T) { return errors.New("force") }, }, - az: az, - err: acme.NewErrorISE("error creating challenge: force"), + az: az, + err: &acme.Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Err: errors.New("error creating challenge: force"), + Detail: "The server experienced an internal error", + Status: 500, + }, } }, "fail/error-db.CreateAuthorization": func(t *testing.T) test { @@ -585,8 +674,101 @@ func TestHandler_newAuthorization(t *testing.T) { return errors.New("force") }, }, - az: az, - err: acme.NewErrorISE("error creating authorization: force"), + az: az, + err: &acme.Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Err: errors.New("error creating authorization: force"), + Detail: "The server experienced an internal error", + Status: 500, + }, + } + }, + "fail/wireapp-user-options": func(t *testing.T) test { + az := &acme.Authorization{ + AccountID: "accID", + Identifier: acme.Identifier{ + Type: "wireapp-user", + Value: "wireapp://%40alice.smith.qa@example.com", + }, + Status: acme.StatusPending, + ExpiresAt: clock.Now(), + } + return test{ + prov: wireProvisionerFailOptions, + db: &acme.MockDB{}, + az: az, + err: &acme.Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Err: errors.New("failed getting Wire options: no Wire options available"), + Detail: "The server experienced an internal error", + Status: 500, + }, + } + }, + "fail/wireapp-device-parse-id": func(t *testing.T) test { + az := &acme.Authorization{ + AccountID: "accID", + Identifier: acme.Identifier{ + Type: "wireapp-device", + Value: `{"name}`, + }, + Status: acme.StatusPending, + ExpiresAt: clock.Now(), + } + return test{ + prov: wireProvisioner, + db: &acme.MockDB{}, + az: az, + err: &acme.Error{ + Type: "urn:ietf:params:acme:error:malformed", + Err: errors.New("failed parsing WireDevice: unexpected end of JSON input"), + Detail: "The request message was malformed", + Status: 400, + }, + } + }, + "fail/wireapp-device-parse-client-id": func(t *testing.T) test { + az := &acme.Authorization{ + AccountID: "accID", + Identifier: acme.Identifier{ + Type: "wireapp-device", + Value: `{"name": "device", "domain": "wire.com", "client-id": "CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`, + }, + Status: acme.StatusPending, + ExpiresAt: clock.Now(), + } + return test{ + prov: wireProvisioner, + db: &acme.MockDB{}, + az: az, + err: &acme.Error{ + Type: "urn:ietf:params:acme:error:malformed", + Err: errors.New(`failed parsing ClientID: invalid Wire client ID scheme ""; expected "wireapp"`), + Detail: "The request message was malformed", + Status: 400, + }, + } + }, + "fail/wireapp-device-options": func(t *testing.T) test { + az := &acme.Authorization{ + AccountID: "accID", + Identifier: acme.Identifier{ + Type: "wireapp-device", + Value: `{"name": "device", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`, + }, + Status: acme.StatusPending, + ExpiresAt: clock.Now(), + } + return test{ + prov: wireProvisionerFailOptions, + db: &acme.MockDB{}, + az: az, + err: &acme.Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Err: errors.New("failed getting Wire options: no Wire options available"), + Detail: "The server experienced an internal error", + Status: 500, + }, } }, "ok/no-wildcard": func(t *testing.T) test { @@ -755,33 +937,121 @@ func TestHandler_newAuthorization(t *testing.T) { az: az, } }, + "ok/wireapp-user": func(t *testing.T) test { + az := &acme.Authorization{ + AccountID: "accID", + Identifier: acme.Identifier{ + Type: "wireapp-user", + Value: "wireapp://%40alice.smith.qa@example.com", + }, + Status: acme.StatusPending, + ExpiresAt: clock.Now(), + } + count := 0 + var ch1 **acme.Challenge + return test{ + prov: wireProvisioner, + db: &acme.MockDB{ + MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { + switch count { + case 0: + ch.ID = "wireapp-user" + assert.Equals(t, ch.Type, acme.WIREOIDC01) + ch1 = &ch + default: + assert.FatalError(t, errors.New("test logic error")) + return errors.New("force") + } + count++ + assert.Equals(t, ch.AccountID, az.AccountID) + assert.Equals(t, ch.Token, az.Token) + assert.Equals(t, ch.Status, acme.StatusPending) + assert.Equals(t, ch.Value, az.Identifier.Value) + return nil + }, + MockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error { + assert.Equals(t, _az.AccountID, az.AccountID) + assert.Equals(t, _az.Token, az.Token) + assert.Equals(t, _az.Status, acme.StatusPending) + assert.Equals(t, _az.Identifier, az.Identifier) + assert.Equals(t, _az.ExpiresAt, az.ExpiresAt) + _ = ch1 + // assert.Equals(t, _az.Challenges, []*acme.Challenge{*ch1}) + assert.Equals(t, _az.Wildcard, false) + return nil + }, + }, + az: az, + } + }, + "ok/wireapp-device": func(t *testing.T) test { + az := &acme.Authorization{ + AccountID: "accID", + Identifier: acme.Identifier{ + Type: "wireapp-device", + Value: `{"name": "device", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}`, + }, + Status: acme.StatusPending, + ExpiresAt: clock.Now(), + } + count := 0 + var ch1 **acme.Challenge + return test{ + prov: wireProvisioner, + db: &acme.MockDB{ + MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { + switch count { + case 0: + ch.ID = "wireapp-device" + assert.Equals(t, ch.Type, acme.WIREDPOP01) + ch1 = &ch + default: + assert.FatalError(t, errors.New("test logic error")) + return errors.New("force") + } + count++ + assert.Equals(t, ch.AccountID, az.AccountID) + assert.Equals(t, ch.Token, az.Token) + assert.Equals(t, ch.Status, acme.StatusPending) + assert.Equals(t, ch.Value, az.Identifier.Value) + return nil + }, + MockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error { + assert.Equals(t, _az.AccountID, az.AccountID) + assert.Equals(t, _az.Token, az.Token) + assert.Equals(t, _az.Status, acme.StatusPending) + assert.Equals(t, _az.Identifier, az.Identifier) + assert.Equals(t, _az.ExpiresAt, az.ExpiresAt) + _ = ch1 + // assert.Equals(t, _az.Challenges, []*acme.Challenge{*ch1}) + assert.Equals(t, _az.Wildcard, false) + return nil + }, + }, + az: az, + } + }, } for name, run := range tests { t.Run(name, func(t *testing.T) { - if name == "ok/permanent-identifier-enabled" { - println(1) - } tc := run(t) ctx := newBaseContext(context.Background(), tc.db) ctx = acme.NewProvisionerContext(ctx, tc.prov) - if err := newAuthorization(ctx, tc.az); err != nil { - if assert.NotNil(t, tc.err) { - var k *acme.Error - if assert.True(t, errors.As(err, &k)) { - assert.Equals(t, k.Type, tc.err.Type) - assert.Equals(t, k.Detail, tc.err.Detail) - assert.Equals(t, k.Status, tc.err.Status) - assert.Equals(t, k.Err.Error(), tc.err.Err.Error()) - assert.Equals(t, k.Detail, tc.err.Detail) - } else { - assert.FatalError(t, errors.New("unexpected error type")) - } + err := newAuthorization(ctx, tc.az) + if tc.err != nil { + sassert.Error(t, err) + var k *acme.Error + if sassert.True(t, errors.As(err, &k)) { + sassert.Equal(t, tc.err.Type, k.Type) + sassert.Equal(t, tc.err.Detail, k.Detail) + sassert.Equal(t, tc.err.Status, k.Status) + sassert.EqualError(t, k.Err, tc.err.Error()) } - } else { - assert.Nil(t, tc.err) + return } - }) + sassert.NoError(t, err) + }) } } @@ -793,6 +1063,10 @@ func TestHandler_NewOrder(t *testing.T) { u := fmt.Sprintf("%s/acme/%s/order/ordID", baseURL.String(), escProvName) + fakeWireSigningKey := `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----` + type test struct { ca acme.CertificateAuthority db acme.DB @@ -1623,6 +1897,141 @@ func TestHandler_NewOrder(t *testing.T) { }, } }, + "ok/default-naf-nbf-wireapp": func(t *testing.T) test { + acmeWireProv := newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wire.Options{ + OIDC: &wire.OIDCOptions{ + Provider: &wire.Provider{ + IssuerURL: "https://issuer.example.com", + AuthURL: "", + TokenURL: "", + JWKSURL: "", + UserInfoURL: "", + Algorithms: []string{"ES256"}, + }, + Config: &wire.Config{ + ClientID: "integration test", + SignatureAlgorithms: []string{"ES256"}, + SkipClientIDCheck: true, + SkipExpiryCheck: true, + SkipIssuerCheck: true, + InsecureSkipSignatureCheck: true, + Now: time.Now, + }, + }, + DPOP: &wire.DPOPOptions{ + SigningKey: []byte(fakeWireSigningKey), + }, + }, + }) + acc := &acme.Account{ID: "accID"} + nor := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "wireapp-user", Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "handle": "wireapp://%40alice_wire@wire.com"}`}, + {Type: "wireapp-device", Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com", "handle": "wireapp://%40alice_wire@wire.com"}`}, + }, + } + b, err := json.Marshal(nor) + assert.FatalError(t, err) + ctx := acme.NewProvisionerContext(context.Background(), acmeWireProv) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + var ( + ch1, ch2 **acme.Challenge + az1ID, az2ID *string + chCount, azCount = 0, 0 + ) + return test{ + ctx: ctx, + statusCode: 201, + nor: nor, + ca: &mockCA{}, + db: &acme.MockDB{ + MockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error { + switch chCount { + case 0: + assert.Equals(t, ch.Type, acme.WIREOIDC01) + assert.Equals(t, ch.Value, `{"name": "Smith, Alice M (QA)", "domain": "example.com", "handle": "wireapp://%40alice_wire@wire.com"}`) + ch.ID = "wireapp-oidc" + ch1 = &ch + case 1: + assert.Equals(t, ch.Type, acme.WIREDPOP01) + assert.Equals(t, ch.Value, `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com", "handle": "wireapp://%40alice_wire@wire.com"}`) + ch.ID = "wireapp-dpop" + ch2 = &ch + default: + require.Fail(t, "test logic error") + } + chCount++ + assert.Equals(t, ch.AccountID, "accID") + assert.NotEquals(t, ch.Token, "") + assert.Equals(t, ch.Status, acme.StatusPending) + + _, _ = ch1, ch2 + + return nil + }, + MockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error { + switch azCount { + case 0: + az.ID = "az1ID" + az1ID = &az.ID + assert.Equals(t, az.Identifier, nor.Identifiers[0]) + assert.Equals(t, az.Challenges, []*acme.Challenge{*ch1}) + case 1: + az.ID = "az2ID" + az2ID = &az.ID + assert.Equals(t, az.Identifier, nor.Identifiers[1]) + assert.Equals(t, az.Challenges, []*acme.Challenge{*ch2}) + default: + require.Fail(t, "test logic error") + } + azCount++ + + assert.Equals(t, az.AccountID, "accID") + assert.NotEquals(t, az.Token, "") + assert.Equals(t, az.Status, acme.StatusPending) + assert.Equals(t, az.Wildcard, false) + return nil + }, + MockCreateOrder: func(ctx context.Context, o *acme.Order) error { + o.ID = "ordID" + assert.Equals(t, o.AccountID, "accID") + assert.Equals(t, o.ProvisionerID, prov.GetID()) + assert.Equals(t, o.Status, acme.StatusPending) + assert.Equals(t, o.Identifiers, nor.Identifiers) + assert.Equals(t, o.AuthorizationIDs, []string{*az1ID, *az2ID}) + return nil + }, + MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return nil, nil + }, + }, + vr: func(t *testing.T, o *acme.Order) { + now := clock.Now() + testBufferDur := 5 * time.Second + orderExpiry := now.Add(defaultOrderExpiry) + expNbf := now.Add(-defaultOrderBackdate) + expNaf := now.Add(prov.DefaultTLSCertDuration()) + + assert.Equals(t, o.ID, "ordID") + assert.Equals(t, o.Status, acme.StatusPending) + assert.Equals(t, o.Identifiers, nor.Identifiers) + assert.Equals(t, o.AuthorizationURLs, []string{ + fmt.Sprintf("%s/acme/%s/authz/az1ID", baseURL.String(), escProvName), + fmt.Sprintf("%s/acme/%s/authz/az2ID", baseURL.String(), escProvName), + }) + assert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf)) + assert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf)) + assert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf)) + assert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf)) + assert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry)) + assert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry)) + }, + } + }, "ok/naf-nbf": func(t *testing.T) test { now := clock.Now() expNbf := now.Add(5 * time.Minute) diff --git a/acme/api/wire_integration_test.go b/acme/api/wire_integration_test.go new file mode 100644 index 00000000..73452e32 --- /dev/null +++ b/acme/api/wire_integration_test.go @@ -0,0 +1,615 @@ +package api + +import ( + "bytes" + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + "time" + + "github.com/go-chi/chi/v5" + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/acme/db/nosql" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/authority/provisioner/wire" + nosqlDB "github.com/smallstep/nosql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.step.sm/crypto/jose" + "go.step.sm/crypto/minica" + "go.step.sm/crypto/pemutil" + "go.step.sm/crypto/x509util" +) + +const ( + baseURL = "test.ca.smallstep.com" + linkerPrefix = "acme" +) + +func newWireProvisionerWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME { + t.Helper() + prov := &provisioner.ACME{ + Type: "ACME", + Name: "test@acme-provisioner.com", + Options: options, + Challenges: []provisioner.ACMEChallenge{ + provisioner.WIREOIDC_01, + provisioner.WIREDPOP_01, + }, + } + + err := prov.Init(provisioner.Config{ + Claims: config.GlobalProvisionerClaims, + }) + require.NoError(t, err) + + return prov +} + +// TODO(hs): replace with test CA server + acmez based test client for +// more realistic integration test? +func TestWireIntegration(t *testing.T) { + accessTokenSignerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + + accessTokenSignerPEMBlock, err := pemutil.Serialize(accessTokenSignerJWK.Public().Key) + require.NoError(t, err) + accessTokenSignerPEMBytes := pem.EncodeToMemory(accessTokenSignerPEMBlock) + + accessTokenSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(accessTokenSignerJWK.Algorithm), + Key: accessTokenSignerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + + oidcTokenSignerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + oidcTokenSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(oidcTokenSignerJWK.Algorithm), + Key: oidcTokenSignerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + + prov := newWireProvisionerWithOptions(t, &provisioner.Options{ + X509: &provisioner.X509Options{ + Template: `{ + "subject": { + "organization": "WireTest", + "commonName": {{ toJson .Oidc.name }} + }, + "uris": [{{ toJson .Oidc.preferred_username }}, {{ toJson .Dpop.sub }}], + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["clientAuth"] + }`, + }, + Wire: &wire.Options{ + OIDC: &wire.OIDCOptions{ + Provider: &wire.Provider{ + IssuerURL: "https://issuer.example.com", + AuthURL: "", + TokenURL: "", + JWKSURL: "", + UserInfoURL: "", + Algorithms: []string{"ES256"}, + }, + Config: &wire.Config{ + ClientID: "integration test", + SignatureAlgorithms: []string{"ES256"}, + SkipClientIDCheck: true, + SkipExpiryCheck: true, + SkipIssuerCheck: true, + InsecureSkipSignatureCheck: true, // NOTE: this skips actual token verification + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wire.DPOPOptions{ + SigningKey: accessTokenSignerPEMBytes, + }, + }, + }) + + // mock provisioner and linker + ctx := context.Background() + ctx = acme.NewProvisionerContext(ctx, prov) + ctx = acme.NewLinkerContext(ctx, acme.NewLinker(baseURL, linkerPrefix)) + + // create temporary BoltDB file + file, err := os.CreateTemp(os.TempDir(), "integration-db-") + require.NoError(t, err) + + t.Log("database file name:", file.Name()) + dbFn := file.Name() + err = file.Close() + require.NoError(t, err) + + // open BoltDB + rawDB, err := nosqlDB.New(nosqlDB.BBoltDriver, dbFn) + require.NoError(t, err) + + // create tables + db, err := nosql.New(rawDB) + require.NoError(t, err) + + // make DB available to handlers + ctx = acme.NewDatabaseContext(ctx, db) + + // simulate signed payloads by making the signing key available in ctx + jwk, err := jose.GenerateJWK("OKP", "", "EdDSA", "sig", "", 0) + require.NoError(t, err) + + ed25519PrivKey, ok := jwk.Key.(ed25519.PrivateKey) + require.True(t, ok) + + dpopSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk, + }, new(jose.SignerOptions)) + require.NoError(t, err) + + ed25519PubKey, ok := ed25519PrivKey.Public().(ed25519.PublicKey) + require.True(t, ok) + + jwk.Key = ed25519PubKey + ctx = context.WithValue(ctx, jwkContextKey, jwk) + + // get directory + dir := func(ctx context.Context) (dir Directory) { + req := httptest.NewRequest(http.MethodGet, "/foo/bar", http.NoBody) + req = req.WithContext(ctx) + w := httptest.NewRecorder() + + GetDirectory(w, req) + res := w.Result() + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + err = json.Unmarshal(bytes.TrimSpace(body), &dir) + require.NoError(t, err) + + return + }(ctx) + t.Log("directory:", dir) + + // get nonce + nonce := func(ctx context.Context) (nonce string) { + req := httptest.NewRequest(http.MethodGet, dir.NewNonce, http.NoBody).WithContext(ctx) + w := httptest.NewRecorder() + addNonce(GetNonce)(w, req) + res := w.Result() + require.Equal(t, http.StatusNoContent, res.StatusCode) + + nonce = res.Header["Replay-Nonce"][0] + return + }(ctx) + t.Log("nonce:", nonce) + + // create new account + acc := func(ctx context.Context) (acc *acme.Account) { + // create payload + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + } + rawNar, err := json.Marshal(nar) + require.NoError(t, err) + + // create account + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: rawNar}) + req := httptest.NewRequest(http.MethodGet, dir.NewAccount, http.NoBody).WithContext(ctx) + w := httptest.NewRecorder() + NewAccount(w, req) + + res := w.Result() + require.Equal(t, http.StatusCreated, res.StatusCode) + + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + require.NoError(t, err) + + err = json.Unmarshal(bytes.TrimSpace(body), &acc) + require.NoError(t, err) + + locationParts := strings.Split(res.Header["Location"][0], "/") + acc, err = db.GetAccount(ctx, locationParts[len(locationParts)-1]) + require.NoError(t, err) + + return + }(ctx) + ctx = context.WithValue(ctx, accContextKey, acc) + t.Log("account ID:", acc.ID) + + // new order + order := func(ctx context.Context) (order *acme.Order) { + mockMustAuthority(t, &mockCA{}) + nor := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + { + Type: "wireapp-user", + Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`, + }, + { + Type: "wireapp-device", + Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`, + }, + }, + } + b, err := json.Marshal(nor) + require.NoError(t, err) + + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + req := httptest.NewRequest("POST", "https://random.local/", http.NoBody) + req = req.WithContext(ctx) + w := httptest.NewRecorder() + NewOrder(w, req) + + res := w.Result() + require.Equal(t, http.StatusCreated, res.StatusCode) + + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + require.NoError(t, err) + + err = json.Unmarshal(bytes.TrimSpace(body), &order) + require.NoError(t, err) + + order, err = db.GetOrder(ctx, order.ID) + require.NoError(t, err) + + return + }(ctx) + t.Log("authzs IDs:", order.AuthorizationIDs) + + // get authorization + getAuthz := func(ctx context.Context, authzID string) (az *acme.Authorization) { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("authzID", authzID) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + + req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx) + w := httptest.NewRecorder() + GetAuthorization(w, req) + + res := w.Result() + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + require.NoError(t, err) + + err = json.Unmarshal(bytes.TrimSpace(body), &az) + require.NoError(t, err) + + az, err = db.GetAuthorization(ctx, authzID) + require.NoError(t, err) + + return + } + var azs []*acme.Authorization + for _, azID := range order.AuthorizationIDs { + az := getAuthz(ctx, azID) + azs = append(azs, az) + for _, challenge := range az.Challenges { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("chID", challenge.ID) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + + var payload []byte + switch challenge.Type { + case acme.WIREDPOP01: + dpopBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Handle string `json:"handle,omitempty"` + Nonce string `json:"nonce,omitempty"` + HTU string `json:"htu,omitempty"` + }{ + Claims: jose.Claims{ + Subject: "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com", + }, + Challenge: "token", + Handle: "wireapp://%40alice.smith.qa@example.com", + Nonce: "nonce", + HTU: "http://issuer.example.com", + }) + require.NoError(t, err) + dpop, err := dpopSigner.Sign(dpopBytes) + require.NoError(t, err) + proof, err := dpop.CompactSerialize() + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Cnf struct { + Kid string `json:"kid,omitempty"` + } `json:"cnf"` + Proof string `json:"proof,omitempty"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` + }{ + Claims: jose.Claims{ + Issuer: "http://issuer.example.com", + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Challenge: "token", + Cnf: struct { + Kid string `json:"kid,omitempty"` + }{ + Kid: jwk.KeyID, + }, + Proof: proof, + ClientID: "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com", + APIVersion: 5, + Scope: "wire_client_id", + }) + + require.NoError(t, err) + signed, err := accessTokenSigner.Sign(tokenBytes) + require.NoError(t, err) + accessToken, err := signed.CompactSerialize() + require.NoError(t, err) + + p, err := json.Marshal(struct { + AccessToken string `json:"access_token"` + }{ + AccessToken: accessToken, + }) + require.NoError(t, err) + payload = p + case acme.WIREOIDC01: + keyAuth, err := acme.KeyAuthorization("token", jwk) + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + }{ + Claims: jose.Claims{ + Issuer: "https://issuer.example.com", + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + }) + require.NoError(t, err) + signed, err := oidcTokenSigner.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + p, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + payload = p + default: + require.Fail(t, "unexpected challenge payload type") + } + + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payload}) + + req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx) + w := httptest.NewRecorder() + GetChallenge(w, req) + + res := w.Result() + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + defer res.Body.Close() //nolint:gocritic // close the body + require.NoError(t, err) + + err = json.Unmarshal(bytes.TrimSpace(body), &challenge) + require.NoError(t, err) + + t.Log("challenge:", challenge.ID, challenge.Status) + } + } + + // get/validate challenge simulation + updateAz := func(ctx context.Context, az *acme.Authorization) (updatedAz *acme.Authorization) { + now := clock.Now().Format(time.RFC3339) + for _, challenge := range az.Challenges { + challenge.Status = acme.StatusValid + challenge.ValidatedAt = now + err := db.UpdateChallenge(ctx, challenge) + if err != nil { + t.Error("updating challenge", challenge.ID, ":", err) + } + } + + updatedAz, err = db.GetAuthorization(ctx, az.ID) + require.NoError(t, err) + + return + } + for _, az := range azs { + updatedAz := updateAz(ctx, az) + for _, challenge := range updatedAz.Challenges { + t.Log("updated challenge:", challenge.ID, challenge.Status) + switch challenge.Type { + case acme.WIREOIDC01: + err = db.CreateOidcToken(ctx, order.ID, map[string]any{"name": "Smith, Alice M (QA)", "preferred_username": "wireapp://%40alice.smith.qa@example.com"}) + require.NoError(t, err) + case acme.WIREDPOP01: + err = db.CreateDpopToken(ctx, order.ID, map[string]any{"sub": "wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com"}) + require.NoError(t, err) + default: + require.Fail(t, "unexpected challenge type") + } + } + } + + // get order + updatedOrder := func(ctx context.Context) (updatedOrder *acme.Order) { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("ordID", order.ID) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + + req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx) + w := httptest.NewRecorder() + GetOrder(w, req) + + res := w.Result() + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + require.NoError(t, err) + + err = json.Unmarshal(bytes.TrimSpace(body), &updatedOrder) + require.NoError(t, err) + + require.Equal(t, acme.StatusReady, updatedOrder.Status) + + return + }(ctx) + t.Log("updated order status:", updatedOrder.Status) + + // finalize order + finalizedOrder := func(ctx context.Context) (finalizedOrder *acme.Order) { + ca, err := minica.New(minica.WithName("WireTestCA")) + require.NoError(t, err) + mockMustAuthority(t, &mockCASigner{ + signer: func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { + var ( + certOptions []x509util.Option + ) + for _, op := range extraOpts { + if k, ok := op.(provisioner.CertificateOptions); ok { + certOptions = append(certOptions, k.Options(signOpts)...) + } + } + + x509utilTemplate, err := x509util.NewCertificate(csr, certOptions...) + require.NoError(t, err) + + template := x509utilTemplate.GetCertificate() + require.NotNil(t, template) + + cert, err := ca.Sign(template) + require.NoError(t, err) + + u1, err := url.Parse("wireapp://%40alice.smith.qa@example.com") + require.NoError(t, err) + u2, err := url.Parse("wireapp://lJGYPz0ZRq2kvc_XpdaDlA%21ed416ce8ecdd9fad@example.com") + require.NoError(t, err) + assert.Equal(t, []*url.URL{u1, u2}, cert.URIs) + assert.Equal(t, "Smith, Alice M (QA)", cert.Subject.CommonName) + + return []*x509.Certificate{cert, ca.Intermediate}, nil + }, + }) + + qUserID, err := url.Parse("wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com") + require.NoError(t, err) + + qUserName, err := url.Parse("wireapp://%40alice.smith.qa@example.com") + require.NoError(t, err) + + _, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + csrTemplate := &x509.CertificateRequest{ + Subject: pkix.Name{ + Organization: []string{"example.com"}, + ExtraNames: []pkix.AttributeTypeAndValue{ + { + Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}, + Value: "Smith, Alice M (QA)", + }, + }, + }, + URIs: []*url.URL{ + qUserName, + qUserID, + }, + SignatureAlgorithm: x509.PureEd25519, + } + + csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, priv) + require.NoError(t, err) + + fr := FinalizeRequest{CSR: base64.RawURLEncoding.EncodeToString(csr)} + frRaw, err := json.Marshal(fr) + require.NoError(t, err) + + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: frRaw}) + + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("ordID", order.ID) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + + req := httptest.NewRequest(http.MethodGet, "https://random.local/", http.NoBody).WithContext(ctx) + w := httptest.NewRecorder() + FinalizeOrder(w, req) + + res := w.Result() + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + require.NoError(t, err) + + err = json.Unmarshal(bytes.TrimSpace(body), &finalizedOrder) + require.NoError(t, err) + + require.Equal(t, acme.StatusValid, finalizedOrder.Status) + + finalizedOrder, err = db.GetOrder(ctx, order.ID) + require.NoError(t, err) + + return + }(ctx) + t.Log("finalized order status:", finalizedOrder.Status) +} + +type mockCASigner struct { + signer func(*x509.CertificateRequest, provisioner.SignOptions, ...provisioner.SignOption) ([]*x509.Certificate, error) +} + +func (m *mockCASigner) SignWithContext(_ context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { + if m.signer == nil { + return nil, errors.New("unimplemented") + } + return m.signer(cr, opts, signOpts...) +} + +func (m *mockCASigner) AreSANsAllowed(ctx context.Context, sans []string) error { + return nil +} + +func (m *mockCASigner) IsRevoked(sn string) (bool, error) { + return false, nil +} + +func (m *mockCASigner) Revoke(ctx context.Context, opts *authority.RevokeOptions) error { + return nil +} + +func (m *mockCASigner) LoadProvisionerByName(string) (provisioner.Interface, error) { + return nil, nil +} diff --git a/acme/challenge.go b/acme/challenge.go index 9f3ca388..4f707713 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -25,18 +25,19 @@ import ( "strings" "time" + "github.com/coreos/go-oidc/v3/oidc" "github.com/fxamacker/cbor/v2" "github.com/google/go-tpm/legacy/tpm2" - "golang.org/x/exp/slices" - "github.com/smallstep/go-attestation/attest" - "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x509util" + "golang.org/x/exp/slices" + "github.com/smallstep/certificates/acme/wire" "github.com/smallstep/certificates/authority/provisioner" + wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire" ) type ChallengeType string @@ -50,6 +51,10 @@ const ( TLSALPN01 ChallengeType = "tls-alpn-01" // DEVICEATTEST01 is the device-attest-01 ACME challenge type DEVICEATTEST01 ChallengeType = "device-attest-01" + // WIREOIDC01 is the Wire OIDC challenge type + WIREOIDC01 ChallengeType = "wire-oidc-01" + // WIREDPOP01 is the Wire DPoP challenge type + WIREDPOP01 ChallengeType = "wire-dpop-01" ) var ( @@ -75,6 +80,7 @@ type Challenge struct { Token string `json:"token"` ValidatedAt string `json:"validated,omitempty"` URL string `json:"url"` + Target string `json:"target,omitempty"` Error *Error `json:"error,omitempty"` } @@ -104,8 +110,12 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, return tlsalpn01Validate(ctx, ch, db, jwk) case DEVICEATTEST01: return deviceAttest01Validate(ctx, ch, db, jwk, payload) + case WIREOIDC01: + return wireOIDC01Validate(ctx, ch, db, jwk, payload) + case WIREDPOP01: + return wireDPOP01Validate(ctx, ch, db, jwk, payload) default: - return NewErrorISE("unexpected challenge type '%s'", ch.Type) + return NewErrorISE("unexpected challenge type %q", ch.Type) } } @@ -342,6 +352,387 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK return nil } +type wireOidcPayload struct { + // IDToken contains the OIDC identity token + IDToken string `json:"id_token"` +} + +func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error { + prov, ok := ProvisionerFromContext(ctx) + if !ok { + return NewErrorISE("missing provisioner") + } + wireOptions, err := prov.GetOptions().GetWireOptions() + if err != nil { + return WrapErrorISE(err, "failed getting Wire options") + } + linker, ok := LinkerFromContext(ctx) + if !ok { + return NewErrorISE("missing linker") + } + + var oidcPayload wireOidcPayload + if err := json.Unmarshal(payload, &oidcPayload); err != nil { + return WrapError(ErrorMalformedType, err, "error unmarshalling Wire OIDC challenge payload") + } + + wireID, err := wire.ParseUserID(ch.Value) + if err != nil { + return WrapErrorISE(err, "error unmarshalling challenge data") + } + + oidcOptions := wireOptions.GetOIDCOptions() + verifier, err := oidcOptions.GetVerifier(ctx) + if err != nil { + return WrapErrorISE(err, "no OIDC verifier available") + } + + idToken, err := verifier.Verify(ctx, oidcPayload.IDToken) + if err != nil { + return storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err, + "error verifying ID token signature")) + } + + var claims struct { + Name string `json:"preferred_username,omitempty"` + Handle string `json:"name"` + Issuer string `json:"iss,omitempty"` + GivenName string `json:"given_name,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud,omitempty"` + } + if err := idToken.Claims(&claims); err != nil { + return storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err, + "error retrieving claims from ID token")) + } + + // TODO(hs): move this into validation below? + expectedKeyAuth, err := KeyAuthorization(ch.Token, jwk) + if err != nil { + return WrapErrorISE(err, "error determining key authorization") + } + if expectedKeyAuth != claims.KeyAuth { + return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, + "keyAuthorization does not match; expected %q, but got %q", expectedKeyAuth, claims.KeyAuth)) + } + + // audience is the full URL to the challenge + acmeAudience := linker.GetLink(ctx, ChallengeLinkType, ch.AuthorizationID, ch.ID) + if claims.ACMEAudience != acmeAudience { + return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, + "invalid 'acme_aud' %q", claims.ACMEAudience)) + } + + transformedIDToken, err := validateWireOIDCClaims(oidcOptions, idToken, wireID) + if err != nil { + return storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err, "claims in OIDC ID token don't match")) + } + + // Update and store the challenge. + ch.Status = StatusValid + ch.Error = nil + ch.ValidatedAt = clock.Now().Format(time.RFC3339) + + if err = db.UpdateChallenge(ctx, ch); err != nil { + return WrapErrorISE(err, "error updating challenge") + } + + orders, err := db.GetAllOrdersByAccountID(ctx, ch.AccountID) + if err != nil { + return WrapErrorISE(err, "could not retrieve current order by account id") + } + if len(orders) == 0 { + return NewErrorISE("there are not enough orders for this account for this custom OIDC challenge") + } + + order := orders[len(orders)-1] + if err := db.CreateOidcToken(ctx, order, transformedIDToken); err != nil { + return WrapErrorISE(err, "failed storing OIDC id token") + } + + return nil +} + +func validateWireOIDCClaims(o *wireprovisioner.OIDCOptions, token *oidc.IDToken, wireID wire.UserID) (map[string]any, error) { + var m map[string]any + if err := token.Claims(&m); err != nil { + return nil, fmt.Errorf("failed extracting OIDC ID token claims: %w", err) + } + transformed, err := o.Transform(m) + if err != nil { + return nil, fmt.Errorf("failed transforming OIDC ID token: %w", err) + } + + name, ok := transformed["name"] + if !ok { + return nil, fmt.Errorf("transformed OIDC ID token does not contain 'name'") + } + if wireID.Name != name { + return nil, fmt.Errorf("invalid 'name' %q after transformation", name) + } + + preferredUsername, ok := transformed["preferred_username"] + if !ok { + return nil, fmt.Errorf("transformed OIDC ID token does not contain 'preferred_username'") + } + if wireID.Handle != preferredUsername { + return nil, fmt.Errorf("invalid 'preferred_username' %q after transformation", preferredUsername) + } + + return transformed, nil +} + +type wireDpopPayload struct { + // AccessToken is the token generated by wire-server + AccessToken string `json:"access_token"` +} + +func wireDPOP01Validate(ctx context.Context, ch *Challenge, db DB, accountJWK *jose.JSONWebKey, payload []byte) error { + prov, ok := ProvisionerFromContext(ctx) + if !ok { + return NewErrorISE("missing provisioner") + } + wireOptions, err := prov.GetOptions().GetWireOptions() + if err != nil { + return WrapErrorISE(err, "failed getting Wire options") + } + linker, ok := LinkerFromContext(ctx) + if !ok { + return NewErrorISE("missing linker") + } + + var dpopPayload wireDpopPayload + if err := json.Unmarshal(payload, &dpopPayload); err != nil { + return WrapError(ErrorMalformedType, err, "error unmarshalling Wire DPoP challenge payload") + } + + wireID, err := wire.ParseDeviceID(ch.Value) + if err != nil { + return WrapErrorISE(err, "error unmarshalling challenge data") + } + + clientID, err := wire.ParseClientID(wireID.ClientID) + if err != nil { + return WrapErrorISE(err, "error parsing device id") + } + + dpopOptions := wireOptions.GetDPOPOptions() + issuer, err := dpopOptions.EvaluateTarget(clientID.DeviceID) + if err != nil { + return WrapErrorISE(err, "invalid Go template registered for 'target'") + } + + // audience is the full URL to the challenge + audience := linker.GetLink(ctx, ChallengeLinkType, ch.AuthorizationID, ch.ID) + + params := wireVerifyParams{ + token: dpopPayload.AccessToken, + tokenKey: dpopOptions.GetSigningKey(), + dpopKey: accountJWK.Public(), + dpopKeyID: accountJWK.KeyID, + issuer: issuer, + audience: audience, + wireID: wireID, + chToken: ch.Token, + t: clock.Now().UTC(), + } + _, dpop, err := parseAndVerifyWireAccessToken(params) + if err != nil { + return storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err, + "failed validating Wire access token")) + } + + // Update and store the challenge. + ch.Status = StatusValid + ch.Error = nil + ch.ValidatedAt = clock.Now().Format(time.RFC3339) + + if err = db.UpdateChallenge(ctx, ch); err != nil { + return WrapErrorISE(err, "error updating challenge") + } + + orders, err := db.GetAllOrdersByAccountID(ctx, ch.AccountID) + if err != nil { + return WrapErrorISE(err, "could not find current order by account id") + } + if len(orders) == 0 { + return NewErrorISE("there are not enough orders for this account for this custom OIDC challenge") + } + + order := orders[len(orders)-1] + if err := db.CreateDpopToken(ctx, order, map[string]any(*dpop)); err != nil { + return WrapErrorISE(err, "failed storing DPoP token") + } + + return nil +} + +type wireCnf struct { + Kid string `json:"kid"` +} + +type wireAccessToken struct { + jose.Claims + Challenge string `json:"chal"` + Nonce string `json:"nonce"` + Cnf wireCnf `json:"cnf"` + Proof string `json:"proof"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` +} + +type wireDpopJwt struct { + jose.Claims + ClientID string `json:"client_id"` + Challenge string `json:"chal"` + Nonce string `json:"nonce"` + HTU string `json:"htu"` +} + +type wireDpopToken map[string]any + +type wireVerifyParams struct { + token string + tokenKey crypto.PublicKey + dpopKey crypto.PublicKey + dpopKeyID string + issuer string + audience string + wireID wire.DeviceID + chToken string + t time.Time +} + +func parseAndVerifyWireAccessToken(v wireVerifyParams) (*wireAccessToken, *wireDpopToken, error) { + jwt, err := jose.ParseSigned(v.token) + if err != nil { + return nil, nil, fmt.Errorf("failed parsing token: %w", err) + } + + if len(jwt.Headers) != 1 { + return nil, nil, fmt.Errorf("token has wrong number of headers %d", len(jwt.Headers)) + } + keyID, err := KeyToID(&jose.JSONWebKey{Key: v.tokenKey}) + if err != nil { + return nil, nil, fmt.Errorf("failed calculating token key ID: %w", err) + } + jwtKeyID := jwt.Headers[0].KeyID + if jwtKeyID == "" { + if jwtKeyID, err = KeyToID(jwt.Headers[0].JSONWebKey); err != nil { + return nil, nil, fmt.Errorf("failed extracting token key ID: %w", err) + } + } + if jwtKeyID != keyID { + return nil, nil, fmt.Errorf("invalid token key ID %q", jwtKeyID) + } + + var accessToken wireAccessToken + if err = jwt.Claims(v.tokenKey, &accessToken); err != nil { + return nil, nil, fmt.Errorf("failed validating Wire DPoP token claims: %w", err) + } + + if err := accessToken.ValidateWithLeeway(jose.Expected{ + Time: v.t, + Issuer: v.issuer, + Audience: jose.Audience{v.audience}, + }, 1*time.Minute); err != nil { + return nil, nil, fmt.Errorf("failed validation: %w", err) + } + + if accessToken.Challenge == "" { + return nil, nil, errors.New("access token challenge 'chal' must not be empty") + } + if accessToken.Cnf.Kid == "" || accessToken.Cnf.Kid != v.dpopKeyID { + return nil, nil, fmt.Errorf("expected 'kid' %q; got %q", v.dpopKeyID, accessToken.Cnf.Kid) + } + if accessToken.ClientID != v.wireID.ClientID { + return nil, nil, fmt.Errorf("invalid Wire 'client_id' %q", accessToken.ClientID) + } + if accessToken.Expiry.Time().After(v.t.Add(time.Hour)) { + return nil, nil, fmt.Errorf("token expiry 'exp' %s is too far into the future", accessToken.Expiry.Time().String()) + } + if accessToken.Scope != "wire_client_id" { + return nil, nil, fmt.Errorf("invalid Wire 'scope' %q", accessToken.Scope) + } + + dpopJWT, err := jose.ParseSigned(accessToken.Proof) + if err != nil { + return nil, nil, fmt.Errorf("invalid Wire DPoP token: %w", err) + } + if len(dpopJWT.Headers) != 1 { + return nil, nil, fmt.Errorf("DPoP token has wrong number of headers %d", len(jwt.Headers)) + } + dpopJwtKeyID := dpopJWT.Headers[0].KeyID + if dpopJwtKeyID == "" { + if dpopJwtKeyID, err = KeyToID(dpopJWT.Headers[0].JSONWebKey); err != nil { + return nil, nil, fmt.Errorf("failed extracting DPoP token key ID: %w", err) + } + } + if dpopJwtKeyID != v.dpopKeyID { + return nil, nil, fmt.Errorf("invalid DPoP token key ID %q", dpopJWT.Headers[0].KeyID) + } + + var wireDpop wireDpopJwt + if err := dpopJWT.Claims(v.dpopKey, &wireDpop); err != nil { + return nil, nil, fmt.Errorf("failed validating Wire DPoP token claims: %w", err) + } + + if err := wireDpop.ValidateWithLeeway(jose.Expected{ + Time: v.t, + Audience: jose.Audience{v.audience}, + }, 1*time.Minute); err != nil { + return nil, nil, fmt.Errorf("failed DPoP validation: %w", err) + } + if wireDpop.HTU == "" || wireDpop.HTU != v.issuer { // DPoP doesn't contains "iss" claim, but has it in the "htu" claim + return nil, nil, fmt.Errorf("DPoP contains invalid issuer 'htu' %q", wireDpop.HTU) + } + if wireDpop.Expiry.Time().After(v.t.Add(time.Hour)) { + return nil, nil, fmt.Errorf("'exp' %s is too far into the future", wireDpop.Expiry.Time().String()) + } + if wireDpop.Subject != v.wireID.ClientID { + return nil, nil, fmt.Errorf("DPoP contains invalid Wire client ID %q", wireDpop.ClientID) + } + if wireDpop.Nonce == "" || wireDpop.Nonce != accessToken.Nonce { + return nil, nil, fmt.Errorf("DPoP contains invalid 'nonce' %q", wireDpop.Nonce) + } + if wireDpop.Challenge == "" || wireDpop.Challenge != accessToken.Challenge { + return nil, nil, fmt.Errorf("DPoP contains invalid challenge 'chal' %q", wireDpop.Challenge) + } + + // TODO(hs): can we use the wireDpopJwt and map that instead of doing Claims() twice? + var dpopToken wireDpopToken + if err := dpopJWT.Claims(v.dpopKey, &dpopToken); err != nil { + return nil, nil, fmt.Errorf("failed validating Wire DPoP token claims: %w", err) + } + + challenge, ok := dpopToken["chal"].(string) + if !ok { + return nil, nil, fmt.Errorf("invalid challenge 'chal' in Wire DPoP token") + } + if challenge == "" || challenge != v.chToken { + return nil, nil, fmt.Errorf("invalid Wire DPoP challenge 'chal' %q", challenge) + } + + handle, ok := dpopToken["handle"].(string) + if !ok { + return nil, nil, fmt.Errorf("invalid 'handle' in Wire DPoP token") + } + if handle == "" || handle != v.wireID.Handle { + return nil, nil, fmt.Errorf("invalid Wire client 'handle' %q", handle) + } + + name, ok := dpopToken["name"].(string) + if !ok { + return nil, nil, fmt.Errorf("invalid display 'name' in Wire DPoP token") + } + if name == "" || name != v.wireID.Name { + return nil, nil, fmt.Errorf("invalid Wire client display 'name' %q", name) + } + + return &accessToken, &dpopToken, nil +} + type payloadType struct { AttObj string `json:"attObj"` Error string `json:"error"` diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 5cede1c5..4f09535d 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -33,11 +33,13 @@ import ( "github.com/fxamacker/cbor/v2" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" + wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/minica" + "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x509util" ) @@ -196,6 +198,25 @@ func mustAttestYubikey(t *testing.T, _, keyAuthorization string, serial int) ([] return payload, leaf, ca.Root } +func newWireProvisionerWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME { + t.Helper() + prov := &provisioner.ACME{ + Type: "ACME", + Name: "wire", + Options: options, + Challenges: []provisioner.ACMEChallenge{ + provisioner.WIREOIDC_01, + provisioner.WIREDPOP_01, + }, + } + if err := prov.Init(provisioner.Config{ + Claims: config.GlobalProvisionerClaims, + }); err != nil { + t.Fatal(err) + } + return prov +} + func Test_storeError(t *testing.T) { type test struct { ch *Challenge @@ -396,6 +417,9 @@ func TestKeyAuthorization(t *testing.T) { } func TestChallenge_Validate(t *testing.T) { + fakeKey := `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----` type test struct { ch *Challenge vc Client @@ -430,7 +454,7 @@ func TestChallenge_Validate(t *testing.T) { } return test{ ch: ch, - err: NewErrorISE("unexpected challenge type 'foo'"), + err: NewErrorISE(`unexpected challenge type "foo"`), } }, "fail/http-01": func(t *testing.T) test { @@ -853,6 +877,263 @@ func TestChallenge_Validate(t *testing.T) { }, } }, + "ok/wire-oidc-01": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return []string{"orderID"}, nil + }, + MockCreateOidcToken: func(ctx context.Context, orderID string, idToken map[string]interface{}) error { + assert.Equal(t, "orderID", orderID) + assert.Equal(t, "Alice Smith", idToken["name"].(string)) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", idToken["preferred_username"].(string)) + return nil + }, + }, + } + }, + "ok/wire-dpop-01": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + _ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation? + dpopSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key) + require.NoError(t, err) + signerPEMBytes := pem.EncodeToMemory(signerPEMBlock) + dpopBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Handle string `json:"handle,omitempty"` + Nonce string `json:"nonce,omitempty"` + HTU string `json:"htu,omitempty"` + Name string `json:"name,omitempty"` + }{ + Claims: jose.Claims{ + Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + }, + Challenge: "token", + Handle: "wireapp://%40alice_wire@wire.com", + Nonce: "nonce", + HTU: "http://issuer.example.com", + Name: "Alice Smith", + }) + require.NoError(t, err) + dpop, err := dpopSigner.Sign(dpopBytes) + require.NoError(t, err) + proof, err := dpop.CompactSerialize() + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Nonce string `json:"nonce,omitempty"` + Cnf struct { + Kid string `json:"kid,omitempty"` + } `json:"cnf"` + Proof string `json:"proof,omitempty"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` + }{ + Claims: jose.Claims{ + Issuer: "http://issuer.example.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Challenge: "token", + Nonce: "nonce", + Cnf: struct { + Kid string `json:"kid,omitempty"` + }{ + Kid: jwk.KeyID, + }, + Proof: proof, + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + APIVersion: 5, + Scope: "wire_client_id", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + accessToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + AccessToken string `json:"access_token"` + }{ + AccessToken: accessToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://issuerexample.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + Target: "http://issuer.example.com", + SigningKey: signerPEMBytes, + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return []string{"orderID"}, nil + }, + MockCreateDpopToken: func(ctx context.Context, orderID string, dpop map[string]interface{}) error { + assert.Equal(t, "orderID", orderID) + assert.Equal(t, "token", dpop["chal"].(string)) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", dpop["handle"].(string)) + assert.Equal(t, "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", dpop["sub"].(string)) + return nil + }, + }, + } + }, } for name, run := range tests { t.Run(name, func(t *testing.T) { @@ -867,25 +1148,63 @@ func TestChallenge_Validate(t *testing.T) { ctx = context.Background() } ctx = NewClientContext(ctx, tc.vc) - if err := tc.ch.Validate(ctx, tc.db, tc.jwk, tc.payload); err != nil { - if assert.Error(t, tc.err) { - var k *Error - if errors.As(err, &k) { - assert.Equal(t, tc.err.Type, k.Type) - assert.Equal(t, tc.err.Detail, k.Detail) - assert.Equal(t, tc.err.Status, k.Status) - assert.Equal(t, tc.err.Err.Error(), k.Err.Error()) - } else { - assert.Fail(t, "unexpected error type") - } + err := tc.ch.Validate(ctx, tc.db, tc.jwk, tc.payload) + if tc.err != nil { + var k *Error + if errors.As(err, &k) { + assert.Equal(t, tc.err.Type, k.Type) + assert.Equal(t, tc.err.Detail, k.Detail) + assert.Equal(t, tc.err.Status, k.Status) + assert.Equal(t, tc.err.Err.Error(), k.Err.Error()) + } else { + assert.Fail(t, "unexpected error type") } - } else { - assert.Nil(t, tc.err) + return } + + assert.NoError(t, err) }) } } +func mustJWKServer(t *testing.T, pub jose.JSONWebKey) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + b, err := json.Marshal(struct { + Keys []jose.JSONWebKey `json:"keys,omitempty"` + }{ + Keys: []jose.JSONWebKey{pub}, + }) + require.NoError(t, err) + jwks := string(b) + + wellKnown := fmt.Sprintf(`{ + "issuer": "%[1]s", + "authorization_endpoint": "%[1]s/auth", + "token_endpoint": "%[1]s/token", + "jwks_uri": "%[1]s/keys", + "userinfo_endpoint": "%[1]s/userinfo", + "id_token_signing_alg_values_supported": ["ES256"] + }`, server.URL) + + mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(w, wellKnown) + if err != nil { + w.WriteHeader(500) + } + }) + mux.HandleFunc("/keys", func(w http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(w, jwks) + if err != nil { + w.WriteHeader(500) + } + }) + + t.Cleanup(server.Close) + return server +} + type errReader int func (errReader) Read([]byte) (int, error) { diff --git a/acme/challenge_wire_test.go b/acme/challenge_wire_test.go new file mode 100644 index 00000000..1ac381ce --- /dev/null +++ b/acme/challenge_wire_test.go @@ -0,0 +1,2281 @@ +package acme + +import ( + "context" + "crypto" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/smallstep/certificates/acme/wire" + "github.com/smallstep/certificates/authority/provisioner" + wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.step.sm/crypto/jose" + "go.step.sm/crypto/pemutil" +) + +func Test_wireDPOP01Validate(t *testing.T) { + fakeKey := `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----` + type test struct { + ch *Challenge + jwk *jose.JSONWebKey + db DB + payload []byte + ctx context.Context + expectedErr *Error + } + tests := map[string]func(t *testing.T) test{ + "fail/no-provisioner": func(t *testing.T) test { + return test{ + ctx: context.Background(), + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New("missing provisioner"), + }, + } + }, + "fail/no-linker": func(t *testing.T) test { + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + return test{ + ctx: ctx, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New("missing linker"), + }, + } + }, + "fail/unmarshal": func(t *testing.T) test { + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ctx: ctx, + payload: []byte("?!"), + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: "1234", + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:malformed", + Detail: "The request message was malformed", + Status: 400, + Err: errors.New(`error unmarshalling Wire DPoP challenge payload: invalid character '?' looking for beginning of value`), + }, + } + }, + "fail/wire-parse-id": func(t *testing.T) test { + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ctx: ctx, + payload: []byte("{}"), + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: "1234", + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`error unmarshalling challenge data: json: cannot unmarshal number into Go value of type wire.DeviceID`), + }, + } + }, + "fail/wire-parse-client-id": func(t *testing.T) test { + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + return test{ + ctx: ctx, + payload: []byte("{}"), + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`error parsing device id: invalid Wire client ID username "594930e9d50bb175"`), + }, + } + }, + "fail/parse-and-verify": func(t *testing.T) test { + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + Target: "{{ .DeviceID }}", + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + jwk, _ := mustAccountAndKeyAuthorization(t, "token") + return test{ + ctx: ctx, + payload: []byte("{}"), + jwk: jwk, + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error { + assert.Equal(t, "chID", ch.ID) + assert.Equal(t, "azID", ch.AuthorizationID) + assert.Equal(t, "accID", ch.AccountID) + assert.Equal(t, "token", ch.Token) + assert.Equal(t, ChallengeType("wire-dpop-01"), ch.Type) + assert.Equal(t, StatusInvalid, ch.Status) + assert.Equal(t, string(valueBytes), ch.Value) + if assert.NotNil(t, ch.Error) { + var k *Error // NOTE: the error is not returned up, but stored with the challenge instead + if errors.As(ch.Error, &k) { + assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) + assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) + assert.Equal(t, 400, k.Status) + assert.Equal(t, `failed validating Wire access token: failed parsing token: go-jose/go-jose: compact JWS format must have three parts`, k.Err.Error()) + } + } + return nil + }, + }, + } + }, + "fail/db.UpdateChallenge": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + _ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation? + dpopSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key) + require.NoError(t, err) + signerPEMBytes := pem.EncodeToMemory(signerPEMBlock) + dpopBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Handle string `json:"handle,omitempty"` + Nonce string `json:"nonce,omitempty"` + HTU string `json:"htu,omitempty"` + Name string `json:"name,omitempty"` + }{ + Claims: jose.Claims{ + Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + }, + Challenge: "token", + Handle: "wireapp://%40alice_wire@wire.com", + Nonce: "nonce", + HTU: "http://issuer.example.com", + Name: "Alice Smith", + }) + require.NoError(t, err) + dpop, err := dpopSigner.Sign(dpopBytes) + require.NoError(t, err) + proof, err := dpop.CompactSerialize() + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Nonce string `json:"nonce,omitempty"` + Cnf struct { + Kid string `json:"kid,omitempty"` + } `json:"cnf"` + Proof string `json:"proof,omitempty"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` + }{ + Claims: jose.Claims{ + Issuer: "http://issuer.example.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Challenge: "token", + Nonce: "nonce", + Cnf: struct { + Kid string `json:"kid,omitempty"` + }{ + Kid: jwk.KeyID, + }, + Proof: proof, + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + APIVersion: 5, + Scope: "wire_client_id", + }) + + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + accessToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + AccessToken string `json:"access_token"` + }{ + AccessToken: accessToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + Target: "http://issuer.example.com", + SigningKey: signerPEMBytes, + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return errors.New("fail") + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`error updating challenge: fail`), + }, + } + }, + "fail/db.GetAllOrdersByAccountID": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + _ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation? + dpopSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key) + require.NoError(t, err) + signerPEMBytes := pem.EncodeToMemory(signerPEMBlock) + dpopBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Handle string `json:"handle,omitempty"` + Nonce string `json:"nonce,omitempty"` + HTU string `json:"htu,omitempty"` + Name string `json:"name,omitempty"` + }{ + Claims: jose.Claims{ + Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + }, + Challenge: "token", + Handle: "wireapp://%40alice_wire@wire.com", + Nonce: "nonce", + HTU: "http://issuer.example.com", + Name: "Alice Smith", + }) + require.NoError(t, err) + dpop, err := dpopSigner.Sign(dpopBytes) + require.NoError(t, err) + proof, err := dpop.CompactSerialize() + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Nonce string `json:"nonce,omitempty"` + Cnf struct { + Kid string `json:"kid,omitempty"` + } `json:"cnf"` + Proof string `json:"proof,omitempty"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` + }{ + Claims: jose.Claims{ + Issuer: "http://issuer.example.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Challenge: "token", + Nonce: "nonce", + Cnf: struct { + Kid string `json:"kid,omitempty"` + }{ + Kid: jwk.KeyID, + }, + Proof: proof, + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + APIVersion: 5, + Scope: "wire_client_id", + }) + + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + accessToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + AccessToken string `json:"access_token"` + }{ + AccessToken: accessToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + Target: "http://issuer.example.com", + SigningKey: signerPEMBytes, + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return nil, errors.New("fail") + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`could not find current order by account id: fail`), + }, + } + }, + "fail/db.GetAllOrdersByAccountID-zero": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + _ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation? + dpopSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key) + require.NoError(t, err) + signerPEMBytes := pem.EncodeToMemory(signerPEMBlock) + dpopBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Handle string `json:"handle,omitempty"` + Nonce string `json:"nonce,omitempty"` + HTU string `json:"htu,omitempty"` + Name string `json:"name,omitempty"` + }{ + Claims: jose.Claims{ + Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + }, + Challenge: "token", + Handle: "wireapp://%40alice_wire@wire.com", + Nonce: "nonce", + HTU: "http://issuer.example.com", + Name: "Alice Smith", + }) + require.NoError(t, err) + dpop, err := dpopSigner.Sign(dpopBytes) + require.NoError(t, err) + proof, err := dpop.CompactSerialize() + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Nonce string `json:"nonce,omitempty"` + Cnf struct { + Kid string `json:"kid,omitempty"` + } `json:"cnf"` + Proof string `json:"proof,omitempty"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` + }{ + Claims: jose.Claims{ + Issuer: "http://issuer.example.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Challenge: "token", + Nonce: "nonce", + Cnf: struct { + Kid string `json:"kid,omitempty"` + }{ + Kid: jwk.KeyID, + }, + Proof: proof, + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + APIVersion: 5, + Scope: "wire_client_id", + }) + + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + accessToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + AccessToken string `json:"access_token"` + }{ + AccessToken: accessToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + Target: "http://issuer.example.com", + SigningKey: signerPEMBytes, + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return []string{}, nil + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`there are not enough orders for this account for this custom OIDC challenge`), + }, + } + }, + "fail/db.CreateDpopToken": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + _ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation? + dpopSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key) + require.NoError(t, err) + signerPEMBytes := pem.EncodeToMemory(signerPEMBlock) + dpopBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Handle string `json:"handle,omitempty"` + Nonce string `json:"nonce,omitempty"` + HTU string `json:"htu,omitempty"` + Name string `json:"name,omitempty"` + }{ + Claims: jose.Claims{ + Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + }, + Challenge: "token", + Handle: "wireapp://%40alice_wire@wire.com", + Nonce: "nonce", + HTU: "http://issuer.example.com", + Name: "Alice Smith", + }) + require.NoError(t, err) + dpop, err := dpopSigner.Sign(dpopBytes) + require.NoError(t, err) + proof, err := dpop.CompactSerialize() + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Nonce string `json:"nonce,omitempty"` + Cnf struct { + Kid string `json:"kid,omitempty"` + } `json:"cnf"` + Proof string `json:"proof,omitempty"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` + }{ + Claims: jose.Claims{ + Issuer: "http://issuer.example.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Challenge: "token", + Nonce: "nonce", + Cnf: struct { + Kid string `json:"kid,omitempty"` + }{ + Kid: jwk.KeyID, + }, + Proof: proof, + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + APIVersion: 5, + Scope: "wire_client_id", + }) + + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + accessToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + AccessToken string `json:"access_token"` + }{ + AccessToken: accessToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + Target: "http://issuer.example.com", + SigningKey: signerPEMBytes, + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return []string{"orderID"}, nil + }, + MockCreateDpopToken: func(ctx context.Context, orderID string, dpop map[string]interface{}) error { + assert.Equal(t, "orderID", orderID) + assert.Equal(t, "token", dpop["chal"].(string)) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", dpop["handle"].(string)) + assert.Equal(t, "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", dpop["sub"].(string)) + return errors.New("fail") + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`failed storing DPoP token: fail`), + }, + } + }, + "ok": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + _ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation? + dpopSigner, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key) + require.NoError(t, err) + signerPEMBytes := pem.EncodeToMemory(signerPEMBlock) + dpopBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Handle string `json:"handle,omitempty"` + Nonce string `json:"nonce,omitempty"` + HTU string `json:"htu,omitempty"` + Name string `json:"name,omitempty"` + }{ + Claims: jose.Claims{ + Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + }, + Challenge: "token", + Handle: "wireapp://%40alice_wire@wire.com", + Nonce: "nonce", + HTU: "http://issuer.example.com", + Name: "Alice Smith", + }) + require.NoError(t, err) + dpop, err := dpopSigner.Sign(dpopBytes) + require.NoError(t, err) + proof, err := dpop.CompactSerialize() + require.NoError(t, err) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Challenge string `json:"chal,omitempty"` + Nonce string `json:"nonce,omitempty"` + Cnf struct { + Kid string `json:"kid,omitempty"` + } `json:"cnf"` + Proof string `json:"proof,omitempty"` + ClientID string `json:"client_id"` + APIVersion int `json:"api_version"` + Scope string `json:"scope"` + }{ + Claims: jose.Claims{ + Issuer: "http://issuer.example.com", + Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Challenge: "token", + Nonce: "nonce", + Cnf: struct { + Kid string `json:"kid,omitempty"` + }{ + Kid: jwk.KeyID, + }, + Proof: proof, + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + APIVersion: 5, + Scope: "wire_client_id", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + accessToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + AccessToken string `json:"access_token"` + }{ + AccessToken: accessToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + Target: "http://issuer.example.com", + SigningKey: signerPEMBytes, + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-dpop-01", + Status: StatusPending, + Value: string(valueBytes), + }, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return []string{"orderID"}, nil + }, + MockCreateDpopToken: func(ctx context.Context, orderID string, dpop map[string]interface{}) error { + assert.Equal(t, "orderID", orderID) + assert.Equal(t, "token", dpop["chal"].(string)) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", dpop["handle"].(string)) + assert.Equal(t, "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", dpop["sub"].(string)) + return nil + }, + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run(t) + err := wireDPOP01Validate(tc.ctx, tc.ch, tc.db, tc.jwk, tc.payload) + if tc.expectedErr != nil { + var k *Error + if errors.As(err, &k) { + assert.Equal(t, tc.expectedErr.Type, k.Type) + assert.Equal(t, tc.expectedErr.Detail, k.Detail) + assert.Equal(t, tc.expectedErr.Status, k.Status) + assert.Equal(t, tc.expectedErr.Err.Error(), k.Err.Error()) + } else { + assert.Fail(t, "unexpected error type") + } + return + } + + assert.NoError(t, err) + }) + } +} + +func Test_wireOIDC01Validate(t *testing.T) { + fakeKey := `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----` + type test struct { + ch *Challenge + jwk *jose.JSONWebKey + db DB + payload []byte + srv *httptest.Server + ctx context.Context + expectedErr *Error + } + tests := map[string]func(t *testing.T) test{ + "fail/no-provisioner": func(t *testing.T) test { + return test{ + ctx: context.Background(), + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New("missing provisioner"), + }, + } + }, + "fail/no-linker": func(t *testing.T) test { + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + return test{ + ctx: ctx, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New("missing linker"), + }, + } + }, + "fail/unmarshal": func(t *testing.T) test { + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ctx: ctx, + payload: []byte("?!"), + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: "1234", + }, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error { + assert.Equal(t, "chID", ch.ID) + return nil + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:malformed", + Detail: "The request message was malformed", + Status: 400, + Err: errors.New(`error unmarshalling Wire OIDC challenge payload: invalid character '?' looking for beginning of value`), + }, + } + }, + "fail/wire-parse-id": func(t *testing.T) test { + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ctx: ctx, + payload: []byte("{}"), + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: "1234", + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`error unmarshalling challenge data: json: cannot unmarshal number into Go value of type wire.UserID`), + }, + } + }, + "fail/verify": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + anotherSignerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + srv := mustJWKServer(t, anotherSignerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusInvalid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + if assert.NotNil(t, updch.Error) { + var k *Error // NOTE: the error is not returned up, but stored with the challenge instead + if errors.As(updch.Error, &k) { + assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) + assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) + assert.Equal(t, 400, k.Status) + assert.Equal(t, `error verifying ID token signature: failed to verify signature: failed to verify id token signature`, k.Err.Error()) + } + } + return nil + }, + }, + } + }, + "fail/keyauth-mismatch": func(t *testing.T) test { + jwk, _ := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: "wrong-keyauth", + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusInvalid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + if assert.NotNil(t, updch.Error) { + var k *Error // NOTE: the error is not returned up, but stored with the challenge instead + if errors.As(updch.Error, &k) { + assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) + assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) + assert.Equal(t, 400, k.Status) + assert.Contains(t, k.Err.Error(), "keyAuthorization does not match") + } + } + return nil + }, + }, + } + }, + "fail/validateWireOIDCClaims": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40bob@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusInvalid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + if assert.NotNil(t, updch.Error) { + var k *Error // NOTE: the error is not returned up, but stored with the challenge instead + if errors.As(updch.Error, &k) { + assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type) + assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail) + assert.Equal(t, 400, k.Status) + assert.Equal(t, `claims in OIDC ID token don't match: invalid 'preferred_username' "wireapp://%40bob@wire.com" after transformation`, k.Err.Error()) + } + } + return nil + }, + }, + } + }, + "fail/db.UpdateChallenge": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return errors.New("fail") + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`error updating challenge: fail`), + }, + } + }, + "fail/db.GetAllOrdersByAccountID": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return nil, errors.New("fail") + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`could not retrieve current order by account id: fail`), + }, + } + }, + "fail/db.GetAllOrdersByAccountID-zero": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return []string{}, nil + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`there are not enough orders for this account for this custom OIDC challenge`), + }, + } + }, + "fail/db.CreateOidcToken": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return []string{"orderID"}, nil + }, + MockCreateOidcToken: func(ctx context.Context, orderID string, idToken map[string]interface{}) error { + assert.Equal(t, "orderID", orderID) + assert.Equal(t, "Alice Smith", idToken["name"].(string)) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", idToken["preferred_username"].(string)) + return errors.New("fail") + }, + }, + expectedErr: &Error{ + Type: "urn:ietf:params:acme:error:serverInternal", + Detail: "The server experienced an internal error", + Status: 500, + Err: errors.New(`failed storing OIDC id token: fail`), + }, + } + }, + "ok/wire-oidc-01": func(t *testing.T) test { + jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token") + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm), + Key: signerJWK, + }, new(jose.SignerOptions)) + require.NoError(t, err) + srv := mustJWKServer(t, signerJWK.Public()) + tokenBytes, err := json.Marshal(struct { + jose.Claims + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + KeyAuth string `json:"keyauth"` + ACMEAudience string `json:"acme_aud"` + }{ + Claims: jose.Claims{ + Issuer: srv.URL, + Audience: []string{"test"}, + Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)), + }, + Name: "Alice Smith", + PreferredUsername: "wireapp://%40alice_wire@wire.com", + KeyAuth: keyAuth, + ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID", + }) + require.NoError(t, err) + signed, err := signer.Sign(tokenBytes) + require.NoError(t, err) + idToken, err := signed.CompactSerialize() + require.NoError(t, err) + payload, err := json.Marshal(struct { + IDToken string `json:"id_token"` + }{ + IDToken: idToken, + }) + require.NoError(t, err) + valueBytes, err := json.Marshal(struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` + }{ + Name: "Alice Smith", + Domain: "wire.com", + ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + }) + require.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{ + Wire: &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: srv.URL, + JWKSURL: srv.URL + "/keys", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: "", + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + }, + })) + ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme")) + return test{ + ch: &Challenge{ + ID: "chID", + AuthorizationID: "azID", + AccountID: "accID", + Token: "token", + Type: "wire-oidc-01", + Status: StatusPending, + Value: string(valueBytes), + }, + srv: srv, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equal(t, "chID", updch.ID) + assert.Equal(t, "token", updch.Token) + assert.Equal(t, StatusValid, updch.Status) + assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type) + assert.Equal(t, string(valueBytes), updch.Value) + return nil + }, + MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) { + assert.Equal(t, "accID", accountID) + return []string{"orderID"}, nil + }, + MockCreateOidcToken: func(ctx context.Context, orderID string, idToken map[string]interface{}) error { + assert.Equal(t, "orderID", orderID) + assert.Equal(t, "Alice Smith", idToken["name"].(string)) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", idToken["preferred_username"].(string)) + return nil + }, + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run(t) + if tc.srv != nil { + defer tc.srv.Close() + } + err := wireOIDC01Validate(tc.ctx, tc.ch, tc.db, tc.jwk, tc.payload) + if tc.expectedErr != nil { + var k *Error + if errors.As(err, &k) { + assert.Equal(t, tc.expectedErr.Type, k.Type) + assert.Equal(t, tc.expectedErr.Detail, k.Detail) + assert.Equal(t, tc.expectedErr.Status, k.Status) + assert.Equal(t, tc.expectedErr.Err.Error(), k.Err.Error()) + } else { + assert.Fail(t, "unexpected error type") + } + return + } + + assert.NoError(t, err) + }) + } +} + +func Test_parseAndVerifyWireAccessToken(t *testing.T) { + t.Skip("skip until we can retrieve public key from e2e test, so that we can actually verify the token") + key := ` +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAB2IYqBWXAouDt3WcCZgCM3t9gumMEKMlgMsGenSu+fA= +-----END PUBLIC KEY-----` + publicKey, err := pemutil.Parse([]byte(key)) + require.NoError(t, err) + pk, ok := publicKey.(ed25519.PublicKey) + require.True(t, ok) + + issuer := "http://wire.com:19983/clients/7a41cf5b79683410/access-token" + wireID := wire.DeviceID{ + ClientID: "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", + Handle: "wireapp://%40alice_wire@wire.com", + } + + token := `eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im8zcWZhQ045a2FzSnZJRlhPdFNMTGhlYW0wTE5jcVF5MHdBMk9PeFRRNW8ifX0.eyJpYXQiOjE3MDU0OTc3MzksImV4cCI6MTcwNTUwMTY5OSwibmJmIjoxNzA1NDk3NzM5LCJpc3MiOiJodHRwOi8vd2lyZS5jb206MTY4MjQvY2xpZW50cy8zN2ZlOThiZDQwZDBkZmUvYWNjZXNzLXRva2VuIiwic3ViIjoid2lyZWFwcDovLzE4NXdIUmtRVHdTOTVGODhaZTQ1SlEhMzdmZTk4YmQ0MGQwZGZlQHdpcmUuY29tIiwiYXVkIjoiaHR0cHM6Ly9zdGVwY2E6NTUwMjMvYWNtZS93aXJlL2NoYWxsZW5nZS9SeEdSWGVoRGxCcHcxNTJQTVUzem0xY2M0cEtGcHVWRi9RWnRFazdQNUVFRXhadHBSYngydjVoYlc3QXB1S2NOSSIsImp0aSI6ImU1MzllODYzLTRkNTgtNGMwMS1iYjk3LTYwODdiNTEzOWIyMCIsIm5vbmNlIjoiUzJKYWVWcExkV28wUkZKaFFrWndXR0ZKY0VoVlFrNUxXVGd4WkhkRFVqQSIsImNoYWwiOiIyaDFPdUdxbTBKUXd6bHVsWGtLSTJEMGZiRDgzRUIxdyIsImNuZiI6eyJraWQiOiJhSEY3MVhYeG0tTWE5Q05zSjNaU1RKTjlYS0ZxOFFmOGh2UTJLN3NLQmQ4In0sInByb29mIjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltUndiM0FyYW5kMElpd2lhbmRySWpwN0ltdDBlU0k2SWs5TFVDSXNJbU55ZGlJNklrVmtNalUxTVRraUxDSjRJam9pWVVsaVMwcFBha0poWXpZeVF6TnRhVmhHVjAxb09ITTJkRXQzUkROaGNHRnVSMHBQZURaVVFYVklRU0o5ZlEuZXlKcFlYUWlPakUzTURVME9UYzNNemtzSW1WNGNDSTZNVGN3TlRVd05Ea3pPU3dpYm1KbUlqb3hOekExTkRrM056TTVMQ0p6ZFdJaU9pSjNhWEpsWVhCd09pOHZNVGcxZDBoU2ExRlVkMU01TlVZNE9GcGxORFZLVVNFek4yWmxPVGhpWkRRd1pEQmtabVZBZDJseVpTNWpiMjBpTENKaGRXUWlPaUpvZEhSd2N6b3ZMM04wWlhCallUbzFOVEF5TXk5aFkyMWxMM2RwY21VdlkyaGhiR3hsYm1kbEwxSjRSMUpZWldoRWJFSndkekUxTWxCTlZUTjZiVEZqWXpSd1MwWndkVlpHTDFGYWRFVnJOMUExUlVWRmVGcDBjRkppZURKMk5XaGlWemRCY0hWTFkwNUpJaXdpYW5ScElqb2lNV1kxTUdRM1lUQXRaamt6WmkwME5XWXdMV0V3TWpBdE1ETm1NREJpTlRreVlUUmtJaXdpYm05dVkyVWlPaUpUTWtwaFpWWndUR1JYYnpCU1JrcG9VV3RhZDFkSFJrcGpSV2hXVVdzMVRGZFVaM2hhU0dSRVZXcEJJaXdpYUhSdElqb2lVRTlUVkNJc0ltaDBkU0k2SW1oMGRIQTZMeTkzYVhKbExtTnZiVG94TmpneU5DOWpiR2xsYm5Sekx6TTNabVU1T0dKa05EQmtNR1JtWlM5aFkyTmxjM010ZEc5clpXNGlMQ0pqYUdGc0lqb2lNbWd4VDNWSGNXMHdTbEYzZW14MWJGaHJTMGt5UkRCbVlrUTRNMFZDTVhjaUxDSm9ZVzVrYkdVaU9pSjNhWEpsWVhCd09pOHZKVFF3WVd4cFkyVmZkMmx5WlVCM2FYSmxMbU52YlNJc0luUmxZVzBpT2lKM2FYSmxJbjAuZlNmQnFuWWlfMTRhZEc5MDAyZ0RJdEgybXNyYW55eXVnR0g5bHpFcmprdmRGbkRPOFRVWWRYUXJKUzdlX3BlU0lzcGxlRUVkaGhzc0gwM3FBWHY2QXciLCJjbGllbnRfaWQiOiJ3aXJlYXBwOi8vMTg1d0hSa1FUd1M5NUY4OFplNDVKUSEzN2ZlOThiZDQwZDBkZmVAd2lyZS5jb20iLCJhcGlfdmVyc2lvbiI6NSwic2NvcGUiOiJ3aXJlX2NsaWVudF9pZCJ9.GKK7ZsJ8EWJjeaHqf8P48H9mluJhxyXUmI0FO3xstda3XDJIK7Z5Ur4hi1OIJB0ZsS5BqRVT2q5whL4KP9hZCA` + ch := &Challenge{ + Token: "bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j", + } + + issuedAtUnix, err := strconv.ParseInt("1704985205", 10, 64) + require.NoError(t, err) + issuedAt := time.Unix(issuedAtUnix, 0) + + jwkBytes := []byte(`{"crv": "Ed25519", "kty": "OKP", "x": "1L1eH2a6AgVvzTp5ZalKRfq6pVPOtEjI7h8TPzBYFgM"}`) + var accountJWK jose.JSONWebKey + json.Unmarshal(jwkBytes, &accountJWK) + + rawKid, err := accountJWK.Thumbprint(crypto.SHA256) + require.NoError(t, err) + accountJWK.KeyID = base64.RawURLEncoding.EncodeToString(rawKid) + + at, dpop, err := parseAndVerifyWireAccessToken(wireVerifyParams{ + token: token, + tokenKey: pk, + dpopKey: accountJWK.Public(), + dpopKeyID: accountJWK.KeyID, + issuer: issuer, + wireID: wireID, + chToken: ch.Token, + t: issuedAt.Add(1 * time.Minute), // set validation time to be one minute after issuance + }) + + if assert.NoError(t, err) { + // token assertions + assert.Equal(t, "42c46d4c-e510-4175-9fb5-d055e125a49d", at.ID) + assert.Equal(t, "http://wire.com:19983/clients/7a41cf5b79683410/access-token", at.Issuer) + assert.Equal(t, "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", at.Subject) + assert.Contains(t, at.Audience, "http://wire.com:19983/clients/7a41cf5b79683410/access-token") + assert.Equal(t, "bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j", at.Challenge) + assert.Equal(t, "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", at.ClientID) + assert.Equal(t, 5, at.APIVersion) + assert.Equal(t, "wire_client_id", at.Scope) + if assert.NotNil(t, at.Cnf) { + assert.Equal(t, "oMWfNDJQsI5cPlXN5UoBNncKtc4f2dq2vwCjjXsqw7Q", at.Cnf.Kid) + } + + // dpop proof assertions + dt := *dpop + assert.Equal(t, "bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j", dt["chal"].(string)) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", dt["handle"].(string)) + assert.Equal(t, "POST", dt["htm"].(string)) + assert.Equal(t, "http://wire.com:19983/clients/7a41cf5b79683410/access-token", dt["htu"].(string)) + assert.Equal(t, "5e6684cb-6b48-468d-b091-ff04bed6ec2e", dt["jti"].(string)) + assert.Equal(t, "UEJyR2dqOEhzZFJEYWJBaTkyODNEYTE2aEs0dHIxcEc", dt["nonce"].(string)) + assert.Equal(t, "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", dt["sub"].(string)) + assert.Equal(t, "wire", dt["team"].(string)) + } +} + +func Test_validateWireOIDCClaims(t *testing.T) { + fakeKey := ` +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----` + opts := &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "http://dex:15818/dex", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "wireapp", + SignatureAlgorithms: []string{"ES256"}, + Now: func() time.Time { + return time.Date(2024, 1, 12, 18, 32, 41, 0, time.UTC) // (Token Expiry: 2024-01-12 21:32:42 +0100 CET) + }, + InsecureSkipSignatureCheck: true, // skipping signature check for this specific test + }, + TransformTemplate: `{"name": "{{ .preferred_username }}", "preferred_username": "{{ .name }}"}`, + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + } + + err := opts.Validate() + require.NoError(t, err) + + idTokenString := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjZhNDZlYzQ3YTQzYWI1ZTc4NzU3MzM5NWY1MGY4ZGQ5MWI2OTM5MzcifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE1ODE4L2RleCIsInN1YiI6IkNqcDNhWEpsWVhCd09pOHZTMmh0VjBOTFpFTlRXakoyT1dWTWFHRk9XVlp6WnlFeU5UZzFNVEpoT0RRek5qTXhaV1V6UUhkcGNtVXVZMjl0RWdSc1pHRnciLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNzA1MDkxNTYyLCJpYXQiOjE3MDUwMDUxNjIsIm5vbmNlIjoib0VjUzBRQUNXLVIyZWkxS09wUmZ2QSIsImF0X2hhc2giOiJoYzk0NmFwS25FeEV5TDVlSzJZMzdRIiwiY19oYXNoIjoidmRubFp2V1d1bVd1Z2NYR1JpOU5FUSIsIm5hbWUiOiJ3aXJlYXBwOi8vJTQwYWxpY2Vfd2lyZUB3aXJlLmNvbSIsInByZWZlcnJlZF91c2VybmFtZSI6IkFsaWNlIFNtaXRoIn0.aEBhWJugBJ9J_0L_4odUCg8SR8HMXVjd__X8uZRo42BSJQQO7-wdpy0jU3S4FOX9fQKr68wD61gS_QsnhfiT7w9U36mLpxaYlNVDCYfpa-gklVFit_0mjUOukXajTLK6H527TGiSss8z22utc40ckS1SbZa2BzKu3yOcqnFHUQwQc5sLYfpRABTB6WBoYFtnWDzdpyWJDaOzz7lfKYv2JBnf9vV8u8SYm-6gNKgtiQ3UUnjhIVUjdfHet2BMvmV2ooZ8V441RULCzKKG_sWZba-D_k_TOnSholGobtUOcKHlmVlmfUe8v7kuyBdhbPcembfgViaNldLQGKZjZfgvLg` + ctx := context.Background() + o := opts.GetOIDCOptions() + verifier, err := o.GetVerifier(ctx) + require.NoError(t, err) + idToken, err := verifier.Verify(ctx, idTokenString) + require.NoError(t, err) + + wireID := wire.UserID{ + Name: "Alice Smith", + Handle: "wireapp://%40alice_wire@wire.com", + } + + got, err := validateWireOIDCClaims(o, idToken, wireID) + assert.NoError(t, err) + + assert.Equal(t, "wireapp://%40alice_wire@wire.com", got["preferred_username"].(string)) + assert.Equal(t, "Alice Smith", got["name"].(string)) + assert.Equal(t, "http://dex:15818/dex", got["iss"].(string)) +} + +func createWireOptions(t *testing.T, transformTemplate string) *wireprovisioner.Options { + t.Helper() + fakeKey := ` +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----` + opts := &wireprovisioner.Options{ + OIDC: &wireprovisioner.OIDCOptions{ + Provider: &wireprovisioner.Provider{ + IssuerURL: "https://issuer.example.com", + Algorithms: []string{"ES256"}, + }, + Config: &wireprovisioner.Config{ + ClientID: "unit test", + SignatureAlgorithms: []string{"ES256"}, + Now: time.Now, + }, + TransformTemplate: transformTemplate, + }, + DPOP: &wireprovisioner.DPOPOptions{ + SigningKey: []byte(fakeKey), + }, + } + + err := opts.Validate() + require.NoError(t, err) + + return opts +} + +func Test_idTokenTransformation(t *testing.T) { + // {"name": "wireapp://%40alice_wire@wire.com", "preferred_username": "Alice Smith", "iss": "http://dex:15818/dex", ...} + idTokenString := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjZhNDZlYzQ3YTQzYWI1ZTc4NzU3MzM5NWY1MGY4ZGQ5MWI2OTM5MzcifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE1ODE4L2RleCIsInN1YiI6IkNqcDNhWEpsWVhCd09pOHZTMmh0VjBOTFpFTlRXakoyT1dWTWFHRk9XVlp6WnlFeU5UZzFNVEpoT0RRek5qTXhaV1V6UUhkcGNtVXVZMjl0RWdSc1pHRnciLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNzA1MDkxNTYyLCJpYXQiOjE3MDUwMDUxNjIsIm5vbmNlIjoib0VjUzBRQUNXLVIyZWkxS09wUmZ2QSIsImF0X2hhc2giOiJoYzk0NmFwS25FeEV5TDVlSzJZMzdRIiwiY19oYXNoIjoidmRubFp2V1d1bVd1Z2NYR1JpOU5FUSIsIm5hbWUiOiJ3aXJlYXBwOi8vJTQwYWxpY2Vfd2lyZUB3aXJlLmNvbSIsInByZWZlcnJlZF91c2VybmFtZSI6IkFsaWNlIFNtaXRoIn0.aEBhWJugBJ9J_0L_4odUCg8SR8HMXVjd__X8uZRo42BSJQQO7-wdpy0jU3S4FOX9fQKr68wD61gS_QsnhfiT7w9U36mLpxaYlNVDCYfpa-gklVFit_0mjUOukXajTLK6H527TGiSss8z22utc40ckS1SbZa2BzKu3yOcqnFHUQwQc5sLYfpRABTB6WBoYFtnWDzdpyWJDaOzz7lfKYv2JBnf9vV8u8SYm-6gNKgtiQ3UUnjhIVUjdfHet2BMvmV2ooZ8V441RULCzKKG_sWZba-D_k_TOnSholGobtUOcKHlmVlmfUe8v7kuyBdhbPcembfgViaNldLQGKZjZfgvLg` + var claims struct { + Name string `json:"name,omitempty"` + Handle string `json:"preferred_username,omitempty"` + Issuer string `json:"iss,omitempty"` + } + + idToken, err := jose.ParseSigned(idTokenString) + require.NoError(t, err) + err = idToken.UnsafeClaimsWithoutVerification(&claims) + require.NoError(t, err) + + // original token contains "Alice Smith" as handle, and name as "wireapp://%40alice_wire@wire.com" + assert.Equal(t, "Alice Smith", claims.Handle) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", claims.Name) + assert.Equal(t, "http://dex:15818/dex", claims.Issuer) + + var m map[string]any + err = idToken.UnsafeClaimsWithoutVerification(&m) + require.NoError(t, err) + + opts := createWireOptions(t, "") // uses default transformation template + result, err := opts.GetOIDCOptions().Transform(m) + require.NoError(t, err) + + // default transformation sets preferred username to handle; name as name + assert.Equal(t, "Alice Smith", result["preferred_username"].(string)) + assert.Equal(t, "wireapp://%40alice_wire@wire.com", result["name"].(string)) + assert.Equal(t, "http://dex:15818/dex", result["iss"].(string)) + + // swap the preferred_name and the name + swap := `{"name": "{{ .preferred_username }}", "preferred_username": "{{ .name }}"}` + opts = createWireOptions(t, swap) + result, err = opts.GetOIDCOptions().Transform(m) + require.NoError(t, err) + + // with the transformation, handle now contains wireapp://%40alice_wire@wire.com, name contains Alice Smith + assert.Equal(t, "wireapp://%40alice_wire@wire.com", result["preferred_username"].(string)) + assert.Equal(t, "Alice Smith", result["name"].(string)) + assert.Equal(t, "http://dex:15818/dex", result["iss"].(string)) +} diff --git a/acme/db.go b/acme/db.go index 4cbb3089..d98234a9 100644 --- a/acme/db.go +++ b/acme/db.go @@ -53,6 +53,13 @@ type DB interface { GetOrder(ctx context.Context, id string) (*Order, error) GetOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) UpdateOrder(ctx context.Context, o *Order) error + + // TODO(hs): put in a different interface + GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) + CreateDpopToken(ctx context.Context, orderID string, dpop map[string]interface{}) error + GetDpopToken(ctx context.Context, orderID string) (map[string]interface{}, error) + CreateOidcToken(ctx context.Context, orderID string, idToken map[string]interface{}) error + GetOidcToken(ctx context.Context, orderID string) (map[string]interface{}, error) } type dbKey struct{} @@ -118,6 +125,12 @@ type MockDB struct { MockGetOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error) MockUpdateOrder func(ctx context.Context, o *Order) error + MockGetAllOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error) + MockGetDpopToken func(ctx context.Context, orderID string) (map[string]interface{}, error) + MockCreateDpopToken func(ctx context.Context, orderID string, dpop map[string]interface{}) error + MockGetOidcToken func(ctx context.Context, orderID string) (map[string]interface{}, error) + MockCreateOidcToken func(ctx context.Context, orderID string, idToken map[string]interface{}) error + MockRet1 interface{} MockError error } @@ -391,3 +404,49 @@ func (m *MockDB) GetOrdersByAccountID(ctx context.Context, accID string) ([]stri } return m.MockRet1.([]string), m.MockError } + +// GetAllOrdersByAccountID returns a list of any order IDs owned by the account. +func (m *MockDB) GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) { + if m.MockGetAllOrdersByAccountID != nil { + return m.MockGetAllOrdersByAccountID(ctx, accountID) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.([]string), m.MockError +} + +// GetDpop retrieves a DPoP from the database. +func (m *MockDB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) { + if m.MockGetDpopToken != nil { + return m.MockGetDpopToken(ctx, orderID) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(map[string]any), m.MockError +} + +// CreateDpop creates DPoP resources and saves them to the DB. +func (m *MockDB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error { + if m.MockCreateDpopToken != nil { + return m.MockCreateDpopToken(ctx, orderID, dpop) + } + return m.MockError +} + +// GetOidcToken retrieves an oidc token from the database. +func (m *MockDB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) { + if m.MockGetOidcToken != nil { + return m.MockGetOidcToken(ctx, orderID) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(map[string]any), m.MockError +} + +// CreateOidcToken creates oidc token resources and saves them to the DB. +func (m *MockDB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error { + if m.MockCreateOidcToken != nil { + return m.MockCreateOidcToken(ctx, orderID, idToken) + } + return m.MockError +} diff --git a/acme/db/nosql/challenge.go b/acme/db/nosql/challenge.go index 9af1ae0d..e7c9aa29 100644 --- a/acme/db/nosql/challenge.go +++ b/acme/db/nosql/challenge.go @@ -19,6 +19,7 @@ type dbChallenge struct { Status acme.Status `json:"status"` Token string `json:"token"` Value string `json:"value"` + Target string `json:"target,omitempty"` ValidatedAt string `json:"validatedAt"` CreatedAt time.Time `json:"createdAt"` Error *acme.Error `json:"error"` // TODO(hs): a bit dangerous; should become db-specific type @@ -61,6 +62,7 @@ func (db *DB) CreateChallenge(ctx context.Context, ch *acme.Challenge) error { Token: ch.Token, CreatedAt: clock.Now(), Type: ch.Type, + Target: ch.Target, } return db.save(ctx, ch.ID, dbch, nil, "challenge", challengeTable) @@ -84,6 +86,7 @@ func (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*acme.Chall Token: dbch.Token, Error: dbch.Error, ValidatedAt: dbch.ValidatedAt, + Target: dbch.Target, } return ch, nil } diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index d19e2987..b2921f55 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -23,6 +23,8 @@ var ( externalAccountKeyTable = []byte("acme_external_account_keys") externalAccountKeyIDsByReferenceTable = []byte("acme_external_account_keyID_reference_index") externalAccountKeyIDsByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index") + wireDpopTokenTable = []byte("wire_acme_dpop_token") + wireOidcTokenTable = []byte("wire_acme_oidc_token") ) // DB is a struct that implements the AcmeDB interface. @@ -36,11 +38,11 @@ func New(db nosqlDB.DB) (*DB, error) { challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, certBySerialTable, externalAccountKeyTable, externalAccountKeyIDsByReferenceTable, externalAccountKeyIDsByProvisionerIDTable, + wireDpopTokenTable, wireOidcTokenTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { - return nil, errors.Wrapf(err, "error creating table %s", - string(b)) + return nil, errors.Wrapf(err, "error creating table %s", string(b)) } } return &DB{db}, nil diff --git a/acme/db/nosql/order.go b/acme/db/nosql/order.go index fc8f2114..983fbe8d 100644 --- a/acme/db/nosql/order.go +++ b/acme/db/nosql/order.go @@ -98,7 +98,7 @@ func (db *DB) CreateOrder(ctx context.Context, o *acme.Order) error { return err } - _, err = db.updateAddOrderIDs(ctx, o.AccountID, o.ID) + _, err = db.updateAddOrderIDs(ctx, o.AccountID, false, o.ID) if err != nil { return err } @@ -117,10 +117,11 @@ func (db *DB) UpdateOrder(ctx context.Context, o *acme.Order) error { nu.Status = o.Status nu.Error = o.Error nu.CertificateID = o.CertificateID + return db.save(ctx, old.ID, nu, old, "order", orderTable) } -func (db *DB) updateAddOrderIDs(ctx context.Context, accID string, addOids ...string) ([]string, error) { +func (db *DB) updateAddOrderIDs(ctx context.Context, accID string, includeReadyOrders bool, addOids ...string) ([]string, error) { ordersByAccountMux.Lock() defer ordersByAccountMux.Unlock() @@ -151,7 +152,8 @@ func (db *DB) updateAddOrderIDs(ctx context.Context, accID string, addOids ...st if err = o.UpdateStatus(ctx, db); err != nil { return nil, acme.WrapErrorISE(err, "error updating order %s for account %s", oid, accID) } - if o.Status == acme.StatusPending { + + if o.Status == acme.StatusPending || (o.Status == acme.StatusReady && includeReadyOrders) { pendOids = append(pendOids, oid) } } @@ -183,5 +185,10 @@ func (db *DB) updateAddOrderIDs(ctx context.Context, accID string, addOids ...st // GetOrdersByAccountID returns a list of order IDs owned by the account. func (db *DB) GetOrdersByAccountID(ctx context.Context, accID string) ([]string, error) { - return db.updateAddOrderIDs(ctx, accID) + return db.updateAddOrderIDs(ctx, accID, false) +} + +// GetAllOrdersByAccountID returns a list of any order IDs owned by the account. +func (db *DB) GetAllOrdersByAccountID(ctx context.Context, accID string) ([]string, error) { + return db.updateAddOrderIDs(ctx, accID, true) } diff --git a/acme/db/nosql/order_test.go b/acme/db/nosql/order_test.go index cf22f094..28bde9c1 100644 --- a/acme/db/nosql/order_test.go +++ b/acme/db/nosql/order_test.go @@ -997,9 +997,9 @@ func TestDB_updateAddOrderIDs(t *testing.T) { err error ) if tc.addOids == nil { - res, err = d.updateAddOrderIDs(context.Background(), accID) + res, err = d.updateAddOrderIDs(context.Background(), accID, false) } else { - res, err = d.updateAddOrderIDs(context.Background(), accID, tc.addOids...) + res, err = d.updateAddOrderIDs(context.Background(), accID, false, tc.addOids...) } if err != nil { diff --git a/acme/db/nosql/wire.go b/acme/db/nosql/wire.go new file mode 100644 index 00000000..03b93505 --- /dev/null +++ b/acme/db/nosql/wire.go @@ -0,0 +1,121 @@ +package nosql + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/smallstep/certificates/acme" + "github.com/smallstep/nosql" +) + +type dbDpopToken struct { + ID string `json:"id"` + Content []byte `json:"content"` + CreatedAt time.Time `json:"createdAt"` +} + +// getDBDpopToken retrieves and unmarshals an DPoP type from the database. +func (db *DB) getDBDpopToken(_ context.Context, orderID string) (*dbDpopToken, error) { + b, err := db.db.Get(wireDpopTokenTable, []byte(orderID)) + if err != nil { + if nosql.IsErrNotFound(err) { + return nil, acme.NewError(acme.ErrorMalformedType, "dpop token %q not found", orderID) + } + return nil, fmt.Errorf("failed loading dpop token %q: %w", orderID, err) + } + + d := new(dbDpopToken) + if err := json.Unmarshal(b, d); err != nil { + return nil, fmt.Errorf("failed unmarshaling dpop token %q into dbDpopToken: %w", orderID, err) + } + return d, nil +} + +// GetDpopToken retrieves an DPoP from the database. +func (db *DB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) { + dbDpop, err := db.getDBDpopToken(ctx, orderID) + if err != nil { + return nil, err + } + + dpop := make(map[string]any) + err = json.Unmarshal(dbDpop.Content, &dpop) + + return dpop, err +} + +// CreateDpopToken creates DPoP resources and saves them to the DB. +func (db *DB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error { + content, err := json.Marshal(dpop) + if err != nil { + return fmt.Errorf("failed marshaling dpop token: %w", err) + } + + now := clock.Now() + dbDpop := &dbDpopToken{ + ID: orderID, + Content: content, + CreatedAt: now, + } + if err := db.save(ctx, orderID, dbDpop, nil, "dpop", wireDpopTokenTable); err != nil { + return fmt.Errorf("failed saving dpop token: %w", err) + } + return nil +} + +type dbOidcToken struct { + ID string `json:"id"` + Content []byte `json:"content"` + CreatedAt time.Time `json:"createdAt"` +} + +// getDBOidcToken retrieves and unmarshals an OIDC id token type from the database. +func (db *DB) getDBOidcToken(_ context.Context, orderID string) (*dbOidcToken, error) { + b, err := db.db.Get(wireOidcTokenTable, []byte(orderID)) + if err != nil { + if nosql.IsErrNotFound(err) { + return nil, acme.NewError(acme.ErrorMalformedType, "oidc token %q not found", orderID) + } + return nil, fmt.Errorf("failed loading oidc token %q: %w", orderID, err) + } + + o := new(dbOidcToken) + if err := json.Unmarshal(b, o); err != nil { + return nil, fmt.Errorf("failed unmarshaling oidc token %q into dbOidcToken: %w", orderID, err) + } + return o, nil +} + +// GetOidcToken retrieves an oidc token from the database. +func (db *DB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) { + dbOidc, err := db.getDBOidcToken(ctx, orderID) + if err != nil { + return nil, err + } + + idToken := make(map[string]any) + err = json.Unmarshal(dbOidc.Content, &idToken) + + return idToken, err +} + +// CreateOidcToken creates oidc token resources and saves them to the DB. +func (db *DB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error { + content, err := json.Marshal(idToken) + if err != nil { + return fmt.Errorf("failed marshaling oidc token: %w", err) + } + + now := clock.Now() + dbOidc := &dbOidcToken{ + ID: orderID, + Content: content, + CreatedAt: now, + } + if err := db.save(ctx, orderID, dbOidc, nil, "oidc", wireOidcTokenTable); err != nil { + return fmt.Errorf("failed saving oidc token: %w", err) + } + return nil +} diff --git a/acme/db/nosql/wire_test.go b/acme/db/nosql/wire_test.go new file mode 100644 index 00000000..6759f420 --- /dev/null +++ b/acme/db/nosql/wire_test.go @@ -0,0 +1,394 @@ +package nosql + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/smallstep/certificates/acme" + certificatesdb "github.com/smallstep/certificates/db" + "github.com/smallstep/nosql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDB_GetDpopToken(t *testing.T) { + type test struct { + db *DB + orderID string + expected map[string]any + expectedErr error + } + var tests = map[string]func(t *testing.T) test{ + "fail/acme-not-found": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + expectedErr: &acme.Error{ + Type: "urn:ietf:params:acme:error:malformed", + Status: 400, + Detail: "The request message was malformed", + Err: errors.New(`dpop token "orderID" not found`), + }, + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + token := dbDpopToken{ + ID: "orderID", + Content: []byte("{}"), + CreatedAt: time.Now(), + } + b, err := json.Marshal(token) + require.NoError(t, err) + err = db.Set(wireDpopTokenTable, []byte("orderID"), b[1:]) // start at index 1; corrupt JSON data + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + expectedErr: errors.New(`failed unmarshaling dpop token "orderID" into dbDpopToken: invalid character ':' after top-level value`), + } + }, + "fail/db.Get": func(t *testing.T) test { + db := &certificatesdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equal(t, wireDpopTokenTable, bucket) + assert.Equal(t, []byte("orderID"), key) + return nil, errors.New("fail") + }, + } + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + expectedErr: errors.New(`failed loading dpop token "orderID": fail`), + } + }, + "ok": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + token := dbDpopToken{ + ID: "orderID", + Content: []byte(`{"sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com"}`), + CreatedAt: time.Now(), + } + b, err := json.Marshal(token) + require.NoError(t, err) + err = db.Set(wireDpopTokenTable, []byte("orderID"), b) + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + expected: map[string]any{ + "sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", + }, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + got, err := tc.db.GetDpopToken(context.Background(), tc.orderID) + if tc.expectedErr != nil { + assert.EqualError(t, err, tc.expectedErr.Error()) + ae := &acme.Error{} + if errors.As(err, &ae) { + ee := &acme.Error{} + require.True(t, errors.As(tc.expectedErr, &ee)) + assert.Equal(t, ee.Detail, ae.Detail) + assert.Equal(t, ee.Type, ae.Type) + assert.Equal(t, ee.Status, ae.Status) + } + assert.Nil(t, got) + return + } + + assert.NoError(t, err) + assert.Equal(t, tc.expected, got) + }) + } +} + +func TestDB_CreateDpopToken(t *testing.T) { + type test struct { + db *DB + orderID string + dpop map[string]any + expectedErr error + } + var tests = map[string]func(t *testing.T) test{ + "fail/db.Save": func(t *testing.T) test { + db := &certificatesdb.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { + assert.Equal(t, wireDpopTokenTable, bucket) + assert.Equal(t, []byte("orderID"), key) + return nil, false, errors.New("fail") + }, + } + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + dpop: map[string]any{ + "sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", + }, + expectedErr: errors.New("failed saving dpop token: error saving acme dpop: fail"), + } + }, + "ok": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + dpop: map[string]any{ + "sub": "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com", + }, + } + }, + "ok/nil": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + dpop: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + err := tc.db.CreateDpopToken(context.Background(), tc.orderID, tc.dpop) + if tc.expectedErr != nil { + assert.EqualError(t, err, tc.expectedErr.Error()) + return + } + + assert.NoError(t, err) + + dpop, err := tc.db.getDBDpopToken(context.Background(), tc.orderID) + require.NoError(t, err) + + assert.Equal(t, tc.orderID, dpop.ID) + var m map[string]any + err = json.Unmarshal(dpop.Content, &m) + require.NoError(t, err) + + assert.Equal(t, tc.dpop, m) + }) + } +} + +func TestDB_GetOidcToken(t *testing.T) { + type test struct { + db *DB + orderID string + expected map[string]any + expectedErr error + } + var tests = map[string]func(t *testing.T) test{ + "fail/acme-not-found": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + expectedErr: &acme.Error{ + Type: "urn:ietf:params:acme:error:malformed", + Status: 400, + Detail: "The request message was malformed", + Err: errors.New(`oidc token "orderID" not found`), + }, + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + token := dbOidcToken{ + ID: "orderID", + Content: []byte("{}"), + CreatedAt: time.Now(), + } + b, err := json.Marshal(token) + require.NoError(t, err) + err = db.Set(wireOidcTokenTable, []byte("orderID"), b[1:]) // start at index 1; corrupt JSON data + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + expectedErr: errors.New(`failed unmarshaling oidc token "orderID" into dbOidcToken: invalid character ':' after top-level value`), + } + }, + "fail/db.Get": func(t *testing.T) test { + db := &certificatesdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equal(t, wireOidcTokenTable, bucket) + assert.Equal(t, []byte("orderID"), key) + return nil, errors.New("fail") + }, + } + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + expectedErr: errors.New(`failed loading oidc token "orderID": fail`), + } + }, + "ok": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + token := dbOidcToken{ + ID: "orderID", + Content: []byte(`{"name": "Alice Smith", "preferred_username": "@alice.smith"}`), + CreatedAt: time.Now(), + } + b, err := json.Marshal(token) + require.NoError(t, err) + err = db.Set(wireOidcTokenTable, []byte("orderID"), b) + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + expected: map[string]any{ + "name": "Alice Smith", + "preferred_username": "@alice.smith", + }, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + got, err := tc.db.GetOidcToken(context.Background(), tc.orderID) + if tc.expectedErr != nil { + assert.EqualError(t, err, tc.expectedErr.Error()) + ae := &acme.Error{} + if errors.As(err, &ae) { + ee := &acme.Error{} + require.True(t, errors.As(tc.expectedErr, &ee)) + assert.Equal(t, ee.Detail, ae.Detail) + assert.Equal(t, ee.Type, ae.Type) + assert.Equal(t, ee.Status, ae.Status) + } + assert.Nil(t, got) + return + } + + assert.NoError(t, err) + assert.Equal(t, tc.expected, got) + }) + } +} + +func TestDB_CreateOidcToken(t *testing.T) { + type test struct { + db *DB + orderID string + oidc map[string]any + expectedErr error + } + var tests = map[string]func(t *testing.T) test{ + "fail/db.Save": func(t *testing.T) test { + db := &certificatesdb.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { + assert.Equal(t, wireOidcTokenTable, bucket) + assert.Equal(t, []byte("orderID"), key) + return nil, false, errors.New("fail") + }, + } + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + oidc: map[string]any{ + "name": "Alice Smith", + "preferred_username": "@alice.smith", + }, + expectedErr: errors.New("failed saving oidc token: error saving acme oidc: fail"), + } + }, + "ok": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + oidc: map[string]any{ + "name": "Alice Smith", + "preferred_username": "@alice.smith", + }, + } + }, + "ok/nil": func(t *testing.T) test { + dir := t.TempDir() + db, err := nosql.New("badgerv2", dir) + require.NoError(t, err) + return test{ + db: &DB{ + db: db, + }, + orderID: "orderID", + oidc: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + err := tc.db.CreateOidcToken(context.Background(), tc.orderID, tc.oidc) + if tc.expectedErr != nil { + assert.EqualError(t, err, tc.expectedErr.Error()) + return + } + + assert.NoError(t, err) + + oidc, err := tc.db.getDBOidcToken(context.Background(), tc.orderID) + require.NoError(t, err) + + assert.Equal(t, tc.orderID, oidc.ID) + var m map[string]any + err = json.Unmarshal(oidc.Content, &m) + require.NoError(t, err) + + assert.Equal(t, tc.oidc, m) + }) + } +} diff --git a/acme/order.go b/acme/order.go index 13c16b27..be38a523 100644 --- a/acme/order.go +++ b/acme/order.go @@ -5,15 +5,20 @@ import ( "context" "crypto/subtle" "crypto/x509" + "encoding/asn1" "encoding/json" + "fmt" "net" + "net/url" "sort" "strings" "time" - "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" + + "github.com/smallstep/certificates/acme/wire" + "github.com/smallstep/certificates/authority/provisioner" ) type IdentifierType string @@ -26,6 +31,10 @@ const ( // PermanentIdentifier is the ACME permanent-identifier identifier type // defined in https://datatracker.ietf.org/doc/html/draft-bweeks-acme-device-attest-00 PermanentIdentifier IdentifierType = "permanent-identifier" + // WireUser is the Wire user identifier type + WireUser IdentifierType = "wireapp-user" + // WireDevice is the Wire device identifier type + WireDevice IdentifierType = "wireapp-device" ) // Identifier encodes the type that an order pertains to. @@ -121,9 +130,11 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { default: return NewErrorISE("unrecognized order status: %s", o.Status) } + if err := db.UpdateOrder(ctx, o); err != nil { return WrapErrorISE(err, "error updating order") } + return nil } @@ -196,7 +207,28 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques // Template data data := x509util.NewTemplateData() - data.SetCommonName(csr.Subject.CommonName) + if o.containsWireIdentifiers() { + subject, err := createWireSubject(o, csr) + if err != nil { + return fmt.Errorf("failed creating Wire subject: %w", err) + } + data.SetSubject(subject) + + // Inject Wire's custom challenges into the template once they have been validated + dpop, err := db.GetDpopToken(ctx, o.ID) + if err != nil { + return fmt.Errorf("failed getting Wire DPoP token: %w", err) + } + data.Set("Dpop", dpop) + + oidc, err := db.GetOidcToken(ctx, o.ID) + if err != nil { + return fmt.Errorf("failed getting Wire OIDC token: %w", err) + } + data.Set("Oidc", oidc) + } else { + data.SetCommonName(csr.Subject.CommonName) + } // Custom sign options passed to authority.Sign var extraOptions []provisioner.SignOption @@ -283,15 +315,76 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques o.CertificateID = cert.ID o.Status = StatusValid + if err = db.UpdateOrder(ctx, o); err != nil { return WrapErrorISE(err, "error updating order %s", o.ID) } + return nil } +// containsWireIdentifiers checks if [Order] contains ACME +// identifiers for the WireUser or WireDevice types. +func (o *Order) containsWireIdentifiers() bool { + for _, i := range o.Identifiers { + if i.Type == WireUser || i.Type == WireDevice { + return true + } + } + return false +} + +// createWireSubject creates the subject for an [Order] with WireUser identifiers. +func createWireSubject(o *Order, csr *x509.CertificateRequest) (subject x509util.Subject, err error) { + wireUserIDs, wireDeviceIDs, otherIDs := 0, 0, 0 + for _, identifier := range o.Identifiers { + switch identifier.Type { + case WireUser: + wireID, err := wire.ParseUserID(identifier.Value) + if err != nil { + return subject, NewErrorISE("unmarshal wireID: %s", err) + } + + // TODO: temporarily using a custom OIDC for carrying the display name without having it listed as a DNS SAN. + // reusing LDAP's OID for diplay name see http://oid-info.com/get/2.16.840.1.113730.3.1.241 + displayNameOid := asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241} + var foundDisplayName = false + for _, entry := range csr.Subject.Names { + if entry.Type.Equal(displayNameOid) { + foundDisplayName = true + displayName := entry.Value.(string) + if displayName != wireID.Name { + return subject, NewErrorISE("expected displayName %v, found %v", wireID.Name, displayName) + } + } + } + if !foundDisplayName { + return subject, NewErrorISE("CSR must contain the display name in '2.16.840.1.113730.3.1.241' OID") + } + + if len(csr.Subject.Organization) == 0 || !strings.EqualFold(csr.Subject.Organization[0], wireID.Domain) { + return subject, NewErrorISE("expected Organization [%s], found %v", wireID.Domain, csr.Subject.Organization) + } + subject.CommonName = wireID.Name + subject.Organization = []string{wireID.Domain} + wireUserIDs++ + case WireDevice: + wireDeviceIDs++ + default: + otherIDs++ + } + } + + if otherIDs > 0 || wireUserIDs != 1 && wireDeviceIDs != 1 { + return subject, NewErrorISE("order must have exactly one WireUser and WireDevice identifier") + } + + return +} + func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativeName, error) { var sans []x509util.SubjectAlternativeName - if len(csr.EmailAddresses) > 0 || len(csr.URIs) > 0 { + if len(csr.EmailAddresses) > 0 { return sans, NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed") } @@ -299,7 +392,8 @@ func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativ orderNames := make([]string, numberOfIdentifierType(DNS, o.Identifiers)) orderIPs := make([]net.IP, numberOfIdentifierType(IP, o.Identifiers)) orderPIDs := make([]string, numberOfIdentifierType(PermanentIdentifier, o.Identifiers)) - indexDNS, indexIP, indexPID := 0, 0, 0 + tmpOrderURIs := make([]*url.URL, numberOfIdentifierType(WireUser, o.Identifiers)+numberOfIdentifierType(WireDevice, o.Identifiers)) + indexDNS, indexIP, indexPID, indexURI := 0, 0, 0, 0 for _, n := range o.Identifiers { switch n.Type { case DNS: @@ -311,14 +405,37 @@ func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativ case PermanentIdentifier: orderPIDs[indexPID] = n.Value indexPID++ + case WireUser: + wireID, err := wire.ParseUserID(n.Value) + if err != nil { + return sans, NewErrorISE("unsupported identifier value in order: %s", n.Value) + } + handle, err := url.Parse(wireID.Handle) + if err != nil { + return sans, NewErrorISE("handle must be a URI: %s", wireID.Handle) + } + tmpOrderURIs[indexURI] = handle + indexURI++ + case WireDevice: + wireID, err := wire.ParseDeviceID(n.Value) + if err != nil { + return sans, NewErrorISE("unsupported identifier value in order: %s", n.Value) + } + clientID, err := url.Parse(wireID.ClientID) + if err != nil { + return sans, NewErrorISE("clientId must be a URI: %s", wireID.ClientID) + } + tmpOrderURIs[indexURI] = clientID + indexURI++ default: return sans, NewErrorISE("unsupported identifier type in order: %s", n.Type) } } orderNames = uniqueSortedLowerNames(orderNames) orderIPs = uniqueSortedIPs(orderIPs) + orderURIs := uniqueSortedURIStrings(tmpOrderURIs) - totalNumberOfSANs := len(csr.DNSNames) + len(csr.IPAddresses) + totalNumberOfSANs := len(csr.DNSNames) + len(csr.IPAddresses) + len(csr.URIs) sans = make([]x509util.SubjectAlternativeName, totalNumberOfSANs) index := 0 @@ -361,6 +478,26 @@ func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativ index++ } + if len(csr.URIs) != len(tmpOrderURIs) { + return sans, NewError(ErrorBadCSRType, "CSR URIs do not match identifiers exactly: "+ + "CSR URIs = %v, Order URIs = %v", csr.URIs, tmpOrderURIs) + } + + // sort URI list + csrURIs := uniqueSortedURIStrings(csr.URIs) + + for i := range csrURIs { + if csrURIs[i] != orderURIs[i] { + return sans, NewError(ErrorBadCSRType, "CSR URIs do not match identifiers exactly: "+ + "CSR URIs = %v, Order URIs = %v", csr.URIs, tmpOrderURIs) + } + sans[index] = x509util.SubjectAlternativeName{ + Type: x509util.URIType, + Value: orderURIs[i], + } + index++ + } + return sans, nil } @@ -430,6 +567,21 @@ func uniqueSortedLowerNames(names []string) (unique []string) { } unique = make([]string, 0, len(nameMap)) for name := range nameMap { + if len(name) > 0 { + unique = append(unique, name) + } + } + sort.Strings(unique) + return +} + +func uniqueSortedURIStrings(uris []*url.URL) (unique []string) { + uriMap := make(map[string]struct{}, len(uris)) + for _, name := range uris { + uriMap[name.String()] = struct{}{} + } + unique = make([]string, 0, len(uriMap)) + for name := range uriMap { unique = append(unique, name) } sort.Strings(unique) diff --git a/acme/order_test.go b/acme/order_test.go index 07372af0..be98bc40 100644 --- a/acme/order_test.go +++ b/acme/order_test.go @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "net" - "net/url" "reflect" "testing" "time" @@ -1702,25 +1701,6 @@ func TestOrder_sans(t *testing.T) { want: []x509util.SubjectAlternativeName{}, err: NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed"), }, - { - name: "fail/invalid-alternative-name-uri", - fields: fields{ - Identifiers: []Identifier{}, - }, - csr: &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: "foo.internal", - }, - URIs: []*url.URL{ - { - Scheme: "https://", - Host: "smallstep.com", - }, - }, - }, - want: []x509util.SubjectAlternativeName{}, - err: NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed"), - }, { name: "fail/error-names-length-mismatch", fields: fields{ diff --git a/acme/wire/id.go b/acme/wire/id.go new file mode 100644 index 00000000..5b77f083 --- /dev/null +++ b/acme/wire/id.go @@ -0,0 +1,91 @@ +package wire + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" +) + +type UserID struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + Handle string `json:"handle,omitempty"` +} + +type DeviceID struct { + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + ClientID string `json:"client-id,omitempty"` + Handle string `json:"handle,omitempty"` +} + +func ParseUserID(value string) (id UserID, err error) { + if err = json.Unmarshal([]byte(value), &id); err != nil { + return + } + + switch { + case id.Handle == "": + err = errors.New("handle must not be empty") + case id.Name == "": + err = errors.New("name must not be empty") + case id.Domain == "": + err = errors.New("domain must not be empty") + } + + return +} + +func ParseDeviceID(value string) (id DeviceID, err error) { + if err = json.Unmarshal([]byte(value), &id); err != nil { + return + } + + switch { + case id.Handle == "": + err = errors.New("handle must not be empty") + case id.Name == "": + err = errors.New("name must not be empty") + case id.Domain == "": + err = errors.New("domain must not be empty") + case id.ClientID == "": + err = errors.New("client-id must not be empty") + } + + return +} + +type ClientID struct { + Scheme string + Username string + DeviceID string + Domain string +} + +// ParseClientID parses a Wire clientID. The ClientID format is as follows: +// +// "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", +// +// where '!' is used as a separator between the user id & device id. +func ParseClientID(clientID string) (ClientID, error) { + clientIDURI, err := url.Parse(clientID) + if err != nil { + return ClientID{}, fmt.Errorf("invalid Wire client ID URI %q: %w", clientID, err) + } + if clientIDURI.Scheme != "wireapp" { + return ClientID{}, fmt.Errorf("invalid Wire client ID scheme %q; expected \"wireapp\"", clientIDURI.Scheme) + } + fullUsername := clientIDURI.User.Username() + parts := strings.SplitN(fullUsername, "!", 2) + if len(parts) != 2 { + return ClientID{}, fmt.Errorf("invalid Wire client ID username %q", fullUsername) + } + return ClientID{ + Scheme: clientIDURI.Scheme, + Username: parts[0], + DeviceID: parts[1], + Domain: clientIDURI.Host, + }, nil +} diff --git a/acme/wire/id_test.go b/acme/wire/id_test.go new file mode 100644 index 00000000..4722bcc2 --- /dev/null +++ b/acme/wire/id_test.go @@ -0,0 +1,100 @@ +package wire + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseUserID(t *testing.T) { + ok := `{"name": "Alice Smith", "domain": "wire.com", "handle": "wireapp://%40alice_wire@wire.com"}` + failJSON := `{"name": }` + emptyHandle := `{"name": "Alice Smith", "domain": "wire.com", "handle": ""}` + emptyName := `{"name": "", "domain": "wire.com", "handle": "wireapp://%40alice_wire@wire.com"}` + emptyDomain := `{"name": "Alice Smith", "domain": "", "handle": "wireapp://%40alice_wire@wire.com"}` + tests := []struct { + name string + value string + wantWireID UserID + wantErr bool + }{ + {name: "ok", value: ok, wantWireID: UserID{Name: "Alice Smith", Domain: "wire.com", Handle: "wireapp://%40alice_wire@wire.com"}}, + {name: "fail/json", value: failJSON, wantErr: true}, + {name: "fail/empty-handle", value: emptyHandle, wantErr: true}, + {name: "fail/empty-name", value: emptyName, wantErr: true}, + {name: "fail/empty-domain", value: emptyDomain, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotWireID, err := ParseUserID(tt.value) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.wantWireID, gotWireID) + }) + } +} + +func TestParseDeviceID(t *testing.T) { + ok := `{"name": "device", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}` + failJSON := `{"name": }` + emptyHandle := `{"name": "device", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": ""}` + emptyName := `{"name": "", "domain": "wire.com", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}` + emptyDomain := `{"name": "device", "domain": "", "client-id": "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", "handle": "wireapp://%40alice_wire@wire.com"}` + emptyClientID := `{"name": "device", "domain": "wire.com", "client-id": "", "handle": "wireapp://%40alice_wire@wire.com"}` + tests := []struct { + name string + value string + wantWireID DeviceID + wantErr bool + }{ + {name: "ok", value: ok, wantWireID: DeviceID{Name: "device", Domain: "wire.com", ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", Handle: "wireapp://%40alice_wire@wire.com"}}, + {name: "fail/json", value: failJSON, wantErr: true}, + {name: "fail/empty-handle", value: emptyHandle, wantErr: true}, + {name: "fail/empty-name", value: emptyName, wantErr: true}, + {name: "fail/empty-domain", value: emptyDomain, wantErr: true}, + {name: "fail/empty-client-id", value: emptyClientID, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotWireID, err := ParseDeviceID(tt.value) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.wantWireID, gotWireID) + }) + } +} + +func TestParseClientID(t *testing.T) { + tests := []struct { + name string + clientID string + want ClientID + expectedErr error + }{ + {name: "ok", clientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", want: ClientID{Scheme: "wireapp", Username: "CzbfFjDOQrenCbDxVmgnFw", DeviceID: "594930e9d50bb175", Domain: "wire.com"}}, + {name: "fail/uri", clientID: "bla", expectedErr: errors.New(`invalid Wire client ID scheme ""; expected "wireapp"`)}, + {name: "fail/scheme", clientID: "not-wireapp://bla.com", expectedErr: errors.New(`invalid Wire client ID scheme "not-wireapp"; expected "wireapp"`)}, + {name: "fail/username", clientID: "wireapp://user@wire.com", expectedErr: errors.New(`invalid Wire client ID username "user"`)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseClientID(tt.clientID) + if tt.expectedErr != nil { + assert.EqualError(t, err, tt.expectedErr.Error()) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 96f37d58..3b7fa654 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -10,6 +10,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme/wire" "go.step.sm/linkedca" ) @@ -26,6 +27,10 @@ const ( TLS_ALPN_01 ACMEChallenge = "tls-alpn-01" // DEVICE_ATTEST_01 is the device-attest-01 ACME challenge. DEVICE_ATTEST_01 ACMEChallenge = "device-attest-01" + // WIREOIDC_01 is the Wire OIDC challenge. + WIREOIDC_01 ACMEChallenge = "wire-oidc-01" + // WIREDPOP_01 is the Wire DPoP challenge. + WIREDPOP_01 ACMEChallenge = "wire-dpop-01" ) // String returns a normalized version of the challenge. @@ -36,7 +41,7 @@ func (c ACMEChallenge) String() string { // Validate returns an error if the acme challenge is not a valid one. func (c ACMEChallenge) Validate() error { switch ACMEChallenge(c.String()) { - case HTTP_01, DNS_01, TLS_ALPN_01, DEVICE_ATTEST_01: + case HTTP_01, DNS_01, TLS_ALPN_01, DEVICE_ATTEST_01, WIREOIDC_01, WIREDPOP_01: return nil default: return fmt.Errorf("acme challenge %q is not supported", c) @@ -102,7 +107,8 @@ type ACME struct { RequireEAB bool `json:"requireEAB,omitempty"` // Challenges contains the enabled challenges for this provisioner. If this // value is not set the default http-01, dns-01 and tls-alpn-01 challenges - // will be enabled, device-attest-01 will be disabled. + // will be enabled, device-attest-01, wire-oidc-01 and wire-dpop-01 will be + // disabled. Challenges []ACMEChallenge `json:"challenges,omitempty"` // AttestationFormats contains the enabled attestation formats for this // provisioner. If this value is not set the default apple, step and tpm @@ -206,10 +212,50 @@ func (p *ACME) Init(config Config) (err error) { } } + if err := p.initializeWireOptions(); err != nil { + return fmt.Errorf("failed initializing Wire options: %w", err) + } + p.ctl, err = NewController(p, p.Claims, config, p.Options) return } +// initializeWireOptions initializes the options for the ACME Wire +// integration. It'll return early if no Wire challenge types are +// enabled. +func (p *ACME) initializeWireOptions() error { + hasWireChallenges := false + for _, c := range p.Challenges { + if c == WIREOIDC_01 || c == WIREDPOP_01 { + hasWireChallenges = true + break + } + } + if !hasWireChallenges { + return nil + } + + w, err := p.GetOptions().GetWireOptions() + if err != nil { + return fmt.Errorf("failed getting Wire options: %w", err) + } + + if err := w.Validate(); err != nil { + return fmt.Errorf("failed validating Wire options: %w", err) + } + + // at this point the Wire options have been validated, and (mostly) + // initialized. Remote keys will be loaded upon the first verification, + // currently. + // TODO(hs): can/should we "prime" the underlying remote keyset, to verify + // auto discovery works as expected? Because of the current way provisioners + // are initialized, doing that as part of the initialization isn't the best + // time to do it, because it could result in operations not resulting in the + // expected result in all cases. + + return nil +} + // ACMEIdentifierType encodes ACME Identifier types type ACMEIdentifierType string @@ -218,6 +264,10 @@ const ( IP ACMEIdentifierType = "ip" // DNS is the ACME dns identifier type DNS ACMEIdentifierType = "dns" + // WireUser is the Wire user identifier type + WireUser ACMEIdentifierType = "wireapp-user" + // WireDevice is the Wire device identifier type + WireDevice ACMEIdentifierType = "wireapp-device" ) // ACMEIdentifier encodes ACME Order Identifiers @@ -243,6 +293,18 @@ func (p *ACME) AuthorizeOrderIdentifier(_ context.Context, identifier ACMEIdenti err = x509Policy.IsIPAllowed(net.ParseIP(identifier.Value)) case DNS: err = x509Policy.IsDNSAllowed(identifier.Value) + case WireUser: + var wireID wire.UserID + if wireID, err = wire.ParseUserID(identifier.Value); err != nil { + return fmt.Errorf("failed parsing Wire SANs: %w", err) + } + err = x509Policy.AreSANsAllowed([]string{wireID.Handle}) + case WireDevice: + var wireID wire.DeviceID + if wireID, err = wire.ParseDeviceID(identifier.Value); err != nil { + return fmt.Errorf("failed parsing Wire SANs: %w", err) + } + err = x509Policy.AreSANsAllowed([]string{wireID.ClientID}) default: err = fmt.Errorf("invalid ACME identifier type '%s' provided", identifier.Type) } diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index 94684ce1..96f4bd8b 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -1,6 +1,3 @@ -//go:build !go1.18 -// +build !go1.18 - package provisioner import ( @@ -14,8 +11,10 @@ import ( "testing" "time" - "github.com/smallstep/assert" "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority/provisioner/wire" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestACMEChallenge_Validate(t *testing.T) { @@ -28,14 +27,20 @@ func TestACMEChallenge_Validate(t *testing.T) { {"dns-01", DNS_01, false}, {"tls-alpn-01", TLS_ALPN_01, false}, {"device-attest-01", DEVICE_ATTEST_01, false}, + {"wire-oidc-01", DEVICE_ATTEST_01, false}, + {"wire-dpop-01", DEVICE_ATTEST_01, false}, {"uppercase", "HTTP-01", false}, {"fail", "http-02", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.c.Validate(); (err != nil) != tt.wantErr { - t.Errorf("ACMEChallenge.Validate() error = %v, wantErr %v", err, tt.wantErr) + err := tt.c.Validate() + if tt.wantErr { + assert.Error(t, err) + return } + + assert.NoError(t, err) }) } } @@ -54,26 +59,24 @@ func TestACMEAttestationFormat_Validate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.f.Validate(); (err != nil) != tt.wantErr { - t.Errorf("ACMEAttestationFormat.Validate() error = %v, wantErr %v", err, tt.wantErr) + err := tt.f.Validate() + if tt.wantErr { + assert.Error(t, err) + return } + + assert.NoError(t, err) }) } } func TestACME_Getters(t *testing.T) { p, err := generateACME() - assert.FatalError(t, err) - id := "acme/" + p.Name - if got := p.GetID(); got != id { - t.Errorf("ACME.GetID() = %v, want %v", got, id) - } - if got := p.GetName(); got != p.Name { - t.Errorf("ACME.GetName() = %v, want %v", got, p.Name) - } - if got := p.GetType(); got != TypeACME { - t.Errorf("ACME.GetType() = %v, want %v", got, TypeACME) - } + require.NoError(t, err) + id := "acme/test@acme-provisioner.com" + assert.Equal(t, id, p.GetID()) + assert.Equal(t, "test@acme-provisioner.com", p.GetName()) + assert.Equal(t, TypeACME, p.GetType()) kid, key, ok := p.GetEncryptedKey() if kid != "" || key != "" || ok == true { t.Errorf("ACME.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)", @@ -83,26 +86,25 @@ func TestACME_Getters(t *testing.T) { func TestACME_Init(t *testing.T) { appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + fakeWireDPoPKey := []byte(`-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----`) type ProvisionerValidateTest struct { p *ACME err error } tests := map[string]func(*testing.T) ProvisionerValidateTest{ - "fail-empty": func(t *testing.T) ProvisionerValidateTest { + "fail/empty": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ p: &ACME{}, err: errors.New("provisioner type cannot be empty"), } }, - "fail-empty-name": func(t *testing.T) ProvisionerValidateTest { + "fail/empty-name": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ p: &ACME{ Type: "ACME", @@ -110,60 +112,119 @@ func TestACME_Init(t *testing.T) { err: errors.New("provisioner name cannot be empty"), } }, - "fail-empty-type": func(t *testing.T) ProvisionerValidateTest { + "fail/empty-type": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ p: &ACME{Name: "foo"}, err: errors.New("provisioner type cannot be empty"), } }, - "fail-bad-claims": func(t *testing.T) ProvisionerValidateTest { + "fail/bad-claims": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &ACME{Name: "foo", Type: "bar", Claims: &Claims{DefaultTLSDur: &Duration{0}}}, + p: &ACME{Name: "foo", Type: "ACME", Claims: &Claims{DefaultTLSDur: &Duration{0}}}, err: errors.New("claims: MinTLSCertDuration must be greater than 0"), } }, - "fail-bad-challenge": func(t *testing.T) ProvisionerValidateTest { + "fail/bad-challenge": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &ACME{Name: "foo", Type: "bar", Challenges: []ACMEChallenge{HTTP_01, "zar"}}, + p: &ACME{Name: "foo", Type: "ACME", Challenges: []ACMEChallenge{HTTP_01, "zar"}}, err: errors.New("acme challenge \"zar\" is not supported"), } }, - "fail-bad-attestation-format": func(t *testing.T) ProvisionerValidateTest { + "fail/bad-attestation-format": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &ACME{Name: "foo", Type: "bar", AttestationFormats: []ACMEAttestationFormat{APPLE, "zar"}}, + p: &ACME{Name: "foo", Type: "ACME", AttestationFormats: []ACMEAttestationFormat{APPLE, "zar"}}, err: errors.New("acme attestation format \"zar\" is not supported"), } }, - "fail-parse-attestation-roots": func(t *testing.T) ProvisionerValidateTest { + "fail/parse-attestation-roots": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &ACME{Name: "foo", Type: "bar", AttestationRoots: []byte("-----BEGIN CERTIFICATE-----\nZm9v\n-----END CERTIFICATE-----")}, + p: &ACME{Name: "foo", Type: "ACME", AttestationRoots: []byte("-----BEGIN CERTIFICATE-----\nZm9v\n-----END CERTIFICATE-----")}, err: errors.New("error parsing attestationRoots: malformed certificate"), } }, - "fail-empty-attestation-roots": func(t *testing.T) ProvisionerValidateTest { + "fail/empty-attestation-roots": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &ACME{Name: "foo", Type: "bar", AttestationRoots: []byte("\n")}, + p: &ACME{Name: "foo", Type: "ACME", AttestationRoots: []byte("\n")}, err: errors.New("error parsing attestationRoots: no certificates found"), } }, + "fail/wire-missing-options": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &ACME{ + Name: "foo", + Type: "ACME", + Challenges: []ACMEChallenge{WIREOIDC_01, WIREDPOP_01}, + }, + err: errors.New("failed initializing Wire options: failed getting Wire options: no options available"), + } + }, + "fail/wire-missing-wire-options": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &ACME{ + Name: "foo", + Type: "ACME", + Challenges: []ACMEChallenge{WIREOIDC_01, WIREDPOP_01}, + Options: &Options{}, + }, + err: errors.New("failed initializing Wire options: failed getting Wire options: no Wire options available"), + } + }, + "fail/wire-validate-options": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &ACME{ + Name: "foo", + Type: "ACME", + Challenges: []ACMEChallenge{WIREOIDC_01, WIREDPOP_01}, + Options: &Options{ + Wire: &wire.Options{ + OIDC: &wire.OIDCOptions{}, + DPOP: &wire.DPOPOptions{ + SigningKey: fakeWireDPoPKey, + }, + }, + }, + }, + err: errors.New("failed initializing Wire options: failed validating Wire options: failed initializing OIDC options: provider not set"), + } + }, "ok": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ - p: &ACME{Name: "foo", Type: "bar"}, + p: &ACME{Name: "foo", Type: "ACME"}, } }, - "ok attestation": func(t *testing.T) ProvisionerValidateTest { + "ok/attestation": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ p: &ACME{ Name: "foo", - Type: "bar", + Type: "ACME", Challenges: []ACMEChallenge{DNS_01, DEVICE_ATTEST_01}, AttestationFormats: []ACMEAttestationFormat{APPLE, STEP}, AttestationRoots: bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n")), }, } }, + "ok/wire": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &ACME{ + Name: "foo", + Type: "ACME", + Challenges: []ACMEChallenge{WIREOIDC_01, WIREDPOP_01}, + Options: &Options{ + Wire: &wire.Options{ + OIDC: &wire.OIDCOptions{ + Provider: &wire.Provider{ + IssuerURL: "https://issuer.example.com", + }, + }, + DPOP: &wire.DPOPOptions{ + SigningKey: fakeWireDPoPKey, + }, + }, + }, + }, + } + }, } - config := Config{ Claims: globalProvisionerClaims, Audiences: testAudiences, @@ -173,13 +234,12 @@ func TestACME_Init(t *testing.T) { tc := get(t) t.Log(string(tc.p.AttestationRoots)) err := tc.p.Init(config) - if err != nil { - if assert.NotNil(t, tc.err) { - assert.Equals(t, tc.err.Error(), err.Error()) - } - } else { - assert.Nil(t, tc.err) + if tc.err != nil { + assert.EqualError(t, err, tc.err.Error()) + return } + + assert.NoError(t, err) }) } } @@ -195,12 +255,12 @@ func TestACME_AuthorizeRenew(t *testing.T) { tests := map[string]func(*testing.T) test{ "fail/renew-disabled": func(t *testing.T) test { p, err := generateACME() - assert.FatalError(t, err) + require.NoError(t, err) // disable renewal disable := true p.Claims = &Claims{DisableRenewal: &disable} p.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims) - assert.FatalError(t, err) + require.NoError(t, err) return test{ p: p, cert: &x509.Certificate{ @@ -213,7 +273,7 @@ func TestACME_AuthorizeRenew(t *testing.T) { }, "ok": func(t *testing.T) test { p, err := generateACME() - assert.FatalError(t, err) + require.NoError(t, err) return test{ p: p, cert: &x509.Certificate{ @@ -226,16 +286,19 @@ func TestACME_AuthorizeRenew(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) + err := tc.p.AuthorizeRenew(context.Background(), tc.cert) + if tc.err != nil { + if assert.Implements(t, (*render.StatusCodedError)(nil), err) { + var sc render.StatusCodedError + if errors.As(err, &sc) { + assert.Equal(t, tc.code, sc.StatusCode()) + } } - } else { - assert.Nil(t, tc.err) + assert.EqualError(t, err, tc.err.Error()) + return } + + assert.NoError(t, err) }) } } @@ -250,7 +313,7 @@ func TestACME_AuthorizeSign(t *testing.T) { tests := map[string]func(*testing.T) test{ "ok": func(t *testing.T) test { p, err := generateACME() - assert.FatalError(t, err) + require.NoError(t, err) return test{ p: p, token: "foo", @@ -260,39 +323,43 @@ func TestACME_AuthorizeSign(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil { - if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) - assert.HasPrefix(t, err.Error(), tc.err.Error()) + opts, err := tc.p.AuthorizeSign(context.Background(), tc.token) + if tc.err != nil { + if assert.Implements(t, (*render.StatusCodedError)(nil), err) { + var sc render.StatusCodedError + if errors.As(err, &sc) { + assert.Equal(t, tc.code, sc.StatusCode()) + } } - } else { - if assert.Nil(t, tc.err) && assert.NotNil(t, opts) { - assert.Equals(t, 8, len(opts)) // number of SignOptions returned - for _, o := range opts { - switch v := o.(type) { - case *ACME: - case *provisionerExtensionOption: - assert.Equals(t, v.Type, TypeACME) - assert.Equals(t, v.Name, tc.p.GetName()) - assert.Equals(t, v.CredentialID, "") - assert.Len(t, 0, v.KeyValuePairs) - case *forceCNOption: - assert.Equals(t, v.ForceCN, tc.p.ForceCN) - case profileDefaultDuration: - assert.Equals(t, time.Duration(v), tc.p.ctl.Claimer.DefaultTLSCertDuration()) - case defaultPublicKeyValidator: - case *validityValidator: - assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration()) - assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration()) - case *x509NamePolicyValidator: - assert.Equals(t, nil, v.policyEngine) - case *WebhookController: - assert.Len(t, 0, v.webhooks) - default: - assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) - } + assert.EqualError(t, err, tc.err.Error()) + return + } + + assert.NoError(t, err) + if assert.NotNil(t, opts) { + assert.Len(t, opts, 8) // number of SignOptions returned + for _, o := range opts { + switch v := o.(type) { + case *ACME: + case *provisionerExtensionOption: + assert.Equal(t, v.Type, TypeACME) + assert.Equal(t, v.Name, tc.p.GetName()) + assert.Equal(t, v.CredentialID, "") + assert.Len(t, v.KeyValuePairs, 0) + case *forceCNOption: + assert.Equal(t, v.ForceCN, tc.p.ForceCN) + case profileDefaultDuration: + assert.Equal(t, time.Duration(v), tc.p.ctl.Claimer.DefaultTLSCertDuration()) + case defaultPublicKeyValidator: + case *validityValidator: + assert.Equal(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration()) + assert.Equal(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration()) + case *x509NamePolicyValidator: + assert.Equal(t, nil, v.policyEngine) + case *WebhookController: + assert.Len(t, v.webhooks, 0) + default: + require.NoError(t, fmt.Errorf("unexpected sign option of type %T", v)) } } } @@ -323,10 +390,14 @@ func TestACME_IsChallengeEnabled(t *testing.T) { {"ok dns-01 enabled", fields{[]ACMEChallenge{"http-01", "dns-01"}}, args{ctx, DNS_01}, true}, {"ok tls-alpn-01 enabled", fields{[]ACMEChallenge{"http-01", "dns-01", "tls-alpn-01"}}, args{ctx, TLS_ALPN_01}, true}, {"ok device-attest-01 enabled", fields{[]ACMEChallenge{"device-attest-01", "dns-01"}}, args{ctx, DEVICE_ATTEST_01}, true}, + {"ok wire-oidc-01 enabled", fields{[]ACMEChallenge{"wire-oidc-01"}}, args{ctx, WIREOIDC_01}, true}, + {"ok wire-dpop-01 enabled", fields{[]ACMEChallenge{"wire-dpop-01"}}, args{ctx, WIREDPOP_01}, true}, {"fail http-01", fields{[]ACMEChallenge{"dns-01"}}, args{ctx, "http-01"}, false}, {"fail dns-01", fields{[]ACMEChallenge{"http-01", "tls-alpn-01"}}, args{ctx, "dns-01"}, false}, {"fail tls-alpn-01", fields{[]ACMEChallenge{"http-01", "dns-01", "device-attest-01"}}, args{ctx, "tls-alpn-01"}, false}, {"fail device-attest-01", fields{[]ACMEChallenge{"http-01", "dns-01"}}, args{ctx, "device-attest-01"}, false}, + {"fail wire-oidc-01", fields{[]ACMEChallenge{"http-01", "dns-01"}}, args{ctx, "wire-oidc-01"}, false}, + {"fail wire-dpop-01", fields{[]ACMEChallenge{"http-01", "dns-01"}}, args{ctx, "wire-dpop-01"}, false}, {"fail unknown", fields{[]ACMEChallenge{"http-01", "dns-01", "tls-alpn-01", "device-attest-01"}}, args{ctx, "unknown"}, false}, } for _, tt := range tests { @@ -334,9 +405,8 @@ func TestACME_IsChallengeEnabled(t *testing.T) { p := &ACME{ Challenges: tt.fields.Challenges, } - if got := p.IsChallengeEnabled(tt.args.ctx, tt.args.challenge); got != tt.want { - t.Errorf("ACME.AuthorizeChallenge() = %v, want %v", got, tt.want) - } + got := p.IsChallengeEnabled(tt.args.ctx, tt.args.challenge) + assert.Equal(t, tt.want, got) }) } } @@ -370,9 +440,8 @@ func TestACME_IsAttestationFormatEnabled(t *testing.T) { p := &ACME{ AttestationFormats: tt.fields.AttestationFormats, } - if got := p.IsAttestationFormatEnabled(tt.args.ctx, tt.args.format); got != tt.want { - t.Errorf("ACME.IsAttestationFormatEnabled() = %v, want %v", got, tt.want) - } + got := p.IsAttestationFormatEnabled(tt.args.ctx, tt.args.format) + assert.Equal(t, tt.want, got) }) } } diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index cbce43de..ec778081 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -11,6 +11,7 @@ import ( "go.step.sm/crypto/x509util" "github.com/smallstep/certificates/authority/policy" + "github.com/smallstep/certificates/authority/provisioner/wire" ) // CertificateOptions is an interface that returns a list of options passed when @@ -30,9 +31,10 @@ func (fn certificateOptionsFunc) Options(so SignOptions) []x509util.Option { type Options struct { X509 *X509Options `json:"x509,omitempty"` SSH *SSHOptions `json:"ssh,omitempty"` - // Webhooks is a list of webhooks that can augment template data Webhooks []*Webhook `json:"webhooks,omitempty"` + // Wire holds the options used for the ACME Wire integration + Wire *wire.Options `json:"wire,omitempty"` } // GetX509Options returns the X.509 options. @@ -51,6 +53,18 @@ func (o *Options) GetSSHOptions() *SSHOptions { return o.SSH } +// GetWireOptions returns the Wire options if available. It +// returns an error if they're not available. +func (o *Options) GetWireOptions() (*wire.Options, error) { + if o == nil { + return nil, errors.New("no options available") + } + if o.Wire == nil { + return nil, errors.New("no Wire options available") + } + return o.Wire, nil +} + // GetWebhooks returns the webhooks options. func (o *Options) GetWebhooks() []*Webhook { if o == nil { diff --git a/authority/provisioner/wire/dpop_options.go b/authority/provisioner/wire/dpop_options.go new file mode 100644 index 00000000..010cd5ee --- /dev/null +++ b/authority/provisioner/wire/dpop_options.go @@ -0,0 +1,49 @@ +package wire + +import ( + "bytes" + "crypto" + "errors" + "fmt" + "text/template" + + "go.step.sm/crypto/pemutil" +) + +type DPOPOptions struct { + // Public part of the signing key for DPoP access token in PEM format + SigningKey []byte `json:"key"` + // URI template for the URI the ACME client must call to fetch the DPoP challenge proof (an access token from wire-server) + Target string `json:"target"` + + signingKey crypto.PublicKey + target *template.Template +} + +func (o *DPOPOptions) GetSigningKey() crypto.PublicKey { + return o.signingKey +} + +func (o *DPOPOptions) EvaluateTarget(deviceID string) (string, error) { + if deviceID == "" { + return "", errors.New("deviceID must not be empty") + } + buf := new(bytes.Buffer) + if err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil { + return "", fmt.Errorf("failed executing DPoP template: %w", err) + } + return buf.String(), nil +} + +func (o *DPOPOptions) validateAndInitialize() (err error) { + o.signingKey, err = pemutil.Parse(o.SigningKey) + if err != nil { + return fmt.Errorf("failed parsing key: %w", err) + } + o.target, err = template.New("DeviceID").Parse(o.Target) + if err != nil { + return fmt.Errorf("failed parsing DPoP template: %w", err) + } + + return nil +} diff --git a/authority/provisioner/wire/dpop_options_test.go b/authority/provisioner/wire/dpop_options_test.go new file mode 100644 index 00000000..68aeb7cf --- /dev/null +++ b/authority/provisioner/wire/dpop_options_test.go @@ -0,0 +1,58 @@ +package wire + +import ( + "errors" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDPOPOptions_EvaluateTarget(t *testing.T) { + tu := "http://wire.com:15958/clients/{{.DeviceID}}/access-token" + target, err := template.New("DeviceID").Parse(tu) + require.NoError(t, err) + fail := "https:/wire.com:15958/clients/{{.DeviceId}}/access-token" + failTarget, err := template.New("DeviceID").Parse(fail) + require.NoError(t, err) + type fields struct { + target *template.Template + } + type args struct { + deviceID string + } + tests := []struct { + name string + fields fields + args args + want string + expectedErr error + }{ + { + name: "ok", fields: fields{target: target}, args: args{deviceID: "deviceID"}, want: "http://wire.com:15958/clients/deviceID/access-token", + }, + { + name: "fail/empty", fields: fields{target: target}, args: args{deviceID: ""}, expectedErr: errors.New("deviceID must not be empty"), + }, + { + name: "fail/template", fields: fields{target: failTarget}, args: args{deviceID: "bla"}, expectedErr: errors.New(`failed executing DPoP template: template: DeviceID:1:32: executing "DeviceID" at <.DeviceId>: can't evaluate field DeviceId in type struct { DeviceID string }`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &DPOPOptions{ + target: tt.fields.target, + } + got, err := o.EvaluateTarget(tt.args.deviceID) + if tt.expectedErr != nil { + assert.EqualError(t, err, tt.expectedErr.Error()) + assert.Empty(t, got) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/authority/provisioner/wire/oidc_options.go b/authority/provisioner/wire/oidc_options.go new file mode 100644 index 00000000..e9139caa --- /dev/null +++ b/authority/provisioner/wire/oidc_options.go @@ -0,0 +1,179 @@ +package wire + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "text/template" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "go.step.sm/crypto/x509util" +) + +type Provider struct { + DiscoveryBaseURL string `json:"discoveryBaseUrl,omitempty"` + IssuerURL string `json:"issuerUrl,omitempty"` + AuthURL string `json:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty"` + JWKSURL string `json:"jwksUrl,omitempty"` + UserInfoURL string `json:"userInfoUrl,omitempty"` + Algorithms []string `json:"signatureAlgorithms,omitempty"` +} + +type Config struct { + ClientID string `json:"clientId,omitempty"` + SignatureAlgorithms []string `json:"signatureAlgorithms,omitempty"` + + // the properties below are only used for testing + SkipClientIDCheck bool `json:"-"` + SkipExpiryCheck bool `json:"-"` + SkipIssuerCheck bool `json:"-"` + InsecureSkipSignatureCheck bool `json:"-"` + Now func() time.Time `json:"-"` +} + +type OIDCOptions struct { + Provider *Provider `json:"provider,omitempty"` + Config *Config `json:"config,omitempty"` + TransformTemplate string `json:"transform,omitempty"` + + target *template.Template + transform *template.Template + oidcProviderConfig *oidc.ProviderConfig + provider *oidc.Provider + verifier *oidc.IDTokenVerifier +} + +func (o *OIDCOptions) GetVerifier(ctx context.Context) (*oidc.IDTokenVerifier, error) { + if o.verifier == nil { + switch { + case o.Provider.DiscoveryBaseURL != "": + // creates a new OIDC provider using automatic discovery and the default HTTP client + provider, err := oidc.NewProvider(ctx, o.Provider.DiscoveryBaseURL) + if err != nil { + return nil, fmt.Errorf("failed creating new OIDC provider using discovery: %w", err) + } + o.provider = provider + default: + o.provider = o.oidcProviderConfig.NewProvider(ctx) + } + + if o.provider == nil { + return nil, errors.New("no OIDC provider available") + } + + o.verifier = o.provider.Verifier(o.getConfig()) + } + + return o.verifier, nil +} + +func (o *OIDCOptions) getConfig() *oidc.Config { + if o == nil || o.Config == nil { + return &oidc.Config{} + } + + return &oidc.Config{ + ClientID: o.Config.ClientID, + SupportedSigningAlgs: o.Config.SignatureAlgorithms, + SkipClientIDCheck: o.Config.SkipClientIDCheck, + SkipExpiryCheck: o.Config.SkipExpiryCheck, + SkipIssuerCheck: o.Config.SkipIssuerCheck, + Now: o.Config.Now, + InsecureSkipSignatureCheck: o.Config.InsecureSkipSignatureCheck, + } +} + +const defaultTemplate = `{"name": "{{ .name }}", "preferred_username": "{{ .preferred_username }}"}` + +func (o *OIDCOptions) validateAndInitialize() (err error) { + if o.Provider == nil { + return errors.New("provider not set") + } + if o.Provider.IssuerURL == "" && o.Provider.DiscoveryBaseURL == "" { + return errors.New("either OIDC discovery or issuer URL must be set") + } + + if o.Provider.DiscoveryBaseURL == "" { + o.oidcProviderConfig, err = toOIDCProviderConfig(o.Provider) + if err != nil { + return fmt.Errorf("failed creationg OIDC provider config: %w", err) + } + } + + o.target, err = template.New("DeviceID").Parse(o.Provider.IssuerURL) + if err != nil { + return fmt.Errorf("failed parsing OIDC template: %w", err) + } + + o.transform, err = parseTransform(o.TransformTemplate) + if err != nil { + return fmt.Errorf("failed parsing OIDC transformation template: %w", err) + } + + return nil +} + +func parseTransform(transformTemplate string) (*template.Template, error) { + if transformTemplate == "" { + transformTemplate = defaultTemplate + } + + return template.New("transform").Funcs(x509util.GetFuncMap()).Parse(transformTemplate) +} + +func (o *OIDCOptions) EvaluateTarget(deviceID string) (string, error) { + buf := new(bytes.Buffer) + if err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil { + return "", fmt.Errorf("failed executing OIDC template: %w", err) + } + return buf.String(), nil +} + +func (o *OIDCOptions) Transform(v map[string]any) (map[string]any, error) { + if o.transform == nil || v == nil { + return v, nil + } + // TODO(hs): add support for extracting error message from template "fail" function? + buf := new(bytes.Buffer) + if err := o.transform.Execute(buf, v); err != nil { + return nil, fmt.Errorf("failed executing OIDC transformation: %w", err) + } + var r map[string]any + if err := json.Unmarshal(buf.Bytes(), &r); err != nil { + return nil, fmt.Errorf("failed unmarshaling transformed OIDC token: %w", err) + } + // add original claims if not yet in the transformed result + for key, value := range v { + if _, ok := r[key]; !ok { + r[key] = value + } + } + return r, nil +} + +func toOIDCProviderConfig(in *Provider) (*oidc.ProviderConfig, error) { + issuerURL, err := url.Parse(in.IssuerURL) + if err != nil { + return nil, fmt.Errorf("failed parsing issuer URL: %w", err) + } + // Removes query params from the URL because we use it as a way to notify client about the actual OAuth ClientId + // for this provisioner. + // This URL is going to look like: "https://idp:5556/dex?clientid=foo" + // If we don't trim the query params here i.e. 'clientid' then the idToken verification is going to fail because + // the 'iss' claim of the idToken will be "https://idp:5556/dex" + issuerURL.RawQuery = "" + issuerURL.Fragment = "" + return &oidc.ProviderConfig{ + IssuerURL: issuerURL.String(), + AuthURL: in.AuthURL, + TokenURL: in.TokenURL, + UserInfoURL: in.UserInfoURL, + JWKSURL: in.JWKSURL, + Algorithms: in.Algorithms, + }, nil +} diff --git a/authority/provisioner/wire/oidc_options_test.go b/authority/provisioner/wire/oidc_options_test.go new file mode 100644 index 00000000..a0bf17e9 --- /dev/null +++ b/authority/provisioner/wire/oidc_options_test.go @@ -0,0 +1,305 @@ +package wire + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "text/template" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.step.sm/crypto/jose" +) + +func TestOIDCOptions_Transform(t *testing.T) { + defaultTransform, err := parseTransform(``) + require.NoError(t, err) + swapTransform, err := parseTransform(`{"name": "{{ .preferred_username }}", "preferred_username": "{{ .name }}"}`) + require.NoError(t, err) + funcTransform, err := parseTransform(`{"name": "{{ .name }}", "preferred_username": "{{ first .usernames }}"}`) + require.NoError(t, err) + type fields struct { + transform *template.Template + } + type args struct { + v map[string]any + } + tests := []struct { + name string + fields fields + args args + want map[string]any + expectedErr error + }{ + { + name: "ok/no-transform", + fields: fields{ + transform: nil, + }, + args: args{ + v: map[string]any{ + "name": "Example", + "preferred_username": "Preferred", + }, + }, + want: map[string]any{ + "name": "Example", + "preferred_username": "Preferred", + }, + }, + { + name: "ok/empty-data", + fields: fields{ + transform: nil, + }, + args: args{ + v: map[string]any{}, + }, + want: map[string]any{}, + }, + { + name: "ok/default-transform", + fields: fields{ + transform: defaultTransform, + }, + args: args{ + v: map[string]any{ + "name": "Example", + "preferred_username": "Preferred", + }, + }, + want: map[string]any{ + "name": "Example", + "preferred_username": "Preferred", + }, + }, + { + name: "ok/swap-transform", + fields: fields{ + transform: swapTransform, + }, + args: args{ + v: map[string]any{ + "name": "Example", + "preferred_username": "Preferred", + }, + }, + want: map[string]any{ + "name": "Preferred", + "preferred_username": "Example", + }, + }, + { + name: "ok/transform-with-functions", + fields: fields{ + transform: funcTransform, + }, + args: args{ + v: map[string]any{ + "name": "Example", + "usernames": []string{"name-1", "name-2", "name-3"}, + }, + }, + want: map[string]any{ + "name": "Example", + "preferred_username": "name-1", + "usernames": []string{"name-1", "name-2", "name-3"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &OIDCOptions{ + transform: tt.fields.transform, + } + got, err := o.Transform(tt.args.v) + if tt.expectedErr != nil { + assert.Error(t, err) + return + } + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestOIDCOptions_EvaluateTarget(t *testing.T) { + tu := "http://target.example.com/{{.DeviceID}}" + target, err := template.New("DeviceID").Parse(tu) + require.NoError(t, err) + empty := "http://target.example.com" + emptyTarget, err := template.New("DeviceID").Parse(empty) + require.NoError(t, err) + fail := "https:/wire.com:15958/clients/{{.DeviceId}}/access-token" + failTarget, err := template.New("DeviceID").Parse(fail) + require.NoError(t, err) + type fields struct { + target *template.Template + } + type args struct { + deviceID string + } + tests := []struct { + name string + fields fields + args args + want string + expectedErr error + }{ + { + name: "ok", fields: fields{target: target}, args: args{deviceID: "deviceID"}, want: "http://target.example.com/deviceID", + }, + { + name: "ok/empty", fields: fields{target: emptyTarget}, args: args{deviceID: ""}, want: "http://target.example.com", + }, + { + name: "fail/template", fields: fields{target: failTarget}, args: args{deviceID: "bla"}, expectedErr: errors.New(`failed executing OIDC template: template: DeviceID:1:32: executing "DeviceID" at <.DeviceId>: can't evaluate field DeviceId in type struct { DeviceID string }`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &OIDCOptions{ + target: tt.fields.target, + } + got, err := o.EvaluateTarget(tt.args.deviceID) + if tt.expectedErr != nil { + assert.EqualError(t, err, tt.expectedErr.Error()) + assert.Empty(t, got) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestOIDCOptions_GetVerifier(t *testing.T) { + signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + require.NoError(t, err) + require.NoError(t, err) + srv := mustDiscoveryServer(t, signerJWK.Public()) + defer srv.Close() + type fields struct { + Provider *Provider + Config *Config + TransformTemplate string + } + tests := []struct { + name string + fields fields + ctx context.Context + want *oidc.IDTokenVerifier + wantErr bool + }{ + { + name: "fail/invalid-discovery-url", + fields: fields{ + Provider: &Provider{ + DiscoveryBaseURL: "http://invalid.example.com", + }, + Config: &Config{ + ClientID: "client-id", + }, + TransformTemplate: "http://target.example.com/{{.DeviceID}}", + }, + ctx: context.Background(), + wantErr: true, + }, + { + name: "ok/auto", + fields: fields{ + Provider: &Provider{ + DiscoveryBaseURL: srv.URL, + }, + Config: &Config{ + ClientID: "client-id", + }, + TransformTemplate: "http://target.example.com/{{.DeviceID}}", + }, + ctx: context.Background(), + }, + { + name: "ok/fixed", + fields: fields{ + Provider: &Provider{ + IssuerURL: "http://issuer.example.com", + }, + Config: &Config{ + ClientID: "client-id", + }, + TransformTemplate: "http://target.example.com/{{.DeviceID}}", + }, + ctx: context.Background(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &OIDCOptions{ + Provider: tt.fields.Provider, + Config: tt.fields.Config, + TransformTemplate: tt.fields.TransformTemplate, + } + + err := o.validateAndInitialize() + require.NoError(t, err) + + verifier, err := o.GetVerifier(tt.ctx) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, verifier) + return + } + + assert.NoError(t, err) + assert.NotNil(t, verifier) + if assert.NotNil(t, o.provider) { + assert.NotNil(t, o.provider.Endpoint()) + } + }) + } +} + +func mustDiscoveryServer(t *testing.T, pub jose.JSONWebKey) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + b, err := json.Marshal(struct { + Keys []jose.JSONWebKey `json:"keys,omitempty"` + }{ + Keys: []jose.JSONWebKey{pub}, + }) + require.NoError(t, err) + jwks := string(b) + + wellKnown := fmt.Sprintf(`{ + "issuer": "%[1]s", + "authorization_endpoint": "%[1]s/auth", + "token_endpoint": "%[1]s/token", + "jwks_uri": "%[1]s/keys", + "userinfo_endpoint": "%[1]s/userinfo", + "id_token_signing_alg_values_supported": ["ES256"] + }`, server.URL) + + mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(w, wellKnown) + if err != nil { + w.WriteHeader(500) + } + }) + mux.HandleFunc("/keys", func(w http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(w, jwks) + if err != nil { + w.WriteHeader(500) + } + }) + + t.Cleanup(server.Close) + return server +} diff --git a/authority/provisioner/wire/wire_options.go b/authority/provisioner/wire/wire_options.go new file mode 100644 index 00000000..2ae5543f --- /dev/null +++ b/authority/provisioner/wire/wire_options.go @@ -0,0 +1,51 @@ +package wire + +import ( + "errors" + "fmt" +) + +// Options holds the Wire ACME extension options +type Options struct { + OIDC *OIDCOptions `json:"oidc,omitempty"` + DPOP *DPOPOptions `json:"dpop,omitempty"` +} + +// GetOIDCOptions returns the OIDC options. +func (o *Options) GetOIDCOptions() *OIDCOptions { + if o == nil { + return nil + } + return o.OIDC +} + +// GetDPOPOptions returns the DPoP options. +func (o *Options) GetDPOPOptions() *DPOPOptions { + if o == nil { + return nil + } + return o.DPOP +} + +// Validate validates and initializes the Wire OIDC and DPoP options. +// +// TODO(hs): find a good way to perform this only once. +func (o *Options) Validate() error { + if oidc := o.GetOIDCOptions(); oidc != nil { + if err := oidc.validateAndInitialize(); err != nil { + return fmt.Errorf("failed initializing OIDC options: %w", err) + } + } else { + return errors.New("no OIDC options available") + } + + if dpop := o.GetDPOPOptions(); dpop != nil { + if err := dpop.validateAndInitialize(); err != nil { + return fmt.Errorf("failed initializing DPoP options: %w", err) + } + } else { + return errors.New("no DPoP options available") + } + + return nil +} diff --git a/authority/provisioner/wire/wire_options_test.go b/authority/provisioner/wire/wire_options_test.go new file mode 100644 index 00000000..c9fc844b --- /dev/null +++ b/authority/provisioner/wire/wire_options_test.go @@ -0,0 +1,163 @@ +package wire + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptions_Validate(t *testing.T) { + key := []byte(`-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k= +-----END PUBLIC KEY-----`) + + type fields struct { + OIDC *OIDCOptions + DPOP *DPOPOptions + } + tests := []struct { + name string + fields fields + expectedErr error + }{ + { + name: "ok", + fields: fields{ + OIDC: &OIDCOptions{ + Provider: &Provider{ + IssuerURL: "https://example.com", + }, + Config: &Config{}, + }, + DPOP: &DPOPOptions{ + SigningKey: key, + }, + }, + expectedErr: nil, + }, + { + name: "fail/no-oidc-options", + fields: fields{ + OIDC: nil, + DPOP: &DPOPOptions{}, + }, + expectedErr: errors.New("no OIDC options available"), + }, + { + name: "fail/empty-issuer-url", + fields: fields{ + OIDC: &OIDCOptions{ + Provider: &Provider{ + IssuerURL: "", + }, + Config: &Config{}, + }, + DPOP: &DPOPOptions{}, + }, + expectedErr: errors.New("failed initializing OIDC options: either OIDC discovery or issuer URL must be set"), + }, + { + name: "fail/invalid-issuer-url", + fields: fields{ + OIDC: &OIDCOptions{ + Provider: &Provider{ + IssuerURL: "\x00", + }, + Config: &Config{}, + }, + DPOP: &DPOPOptions{}, + }, + expectedErr: errors.New(`failed initializing OIDC options: failed creationg OIDC provider config: failed parsing issuer URL: parse "\x00": net/url: invalid control character in URL`), + }, + { + name: "fail/issuer-url-template", + fields: fields{ + OIDC: &OIDCOptions{ + Provider: &Provider{ + IssuerURL: "https://issuer.example.com/{{}", + }, + Config: &Config{}, + }, + DPOP: &DPOPOptions{}, + }, + expectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC template: template: DeviceID:1: unexpected "}" in command`), + }, + { + name: "fail/invalid-transform-template", + fields: fields{ + OIDC: &OIDCOptions{ + Provider: &Provider{ + IssuerURL: "https://example.com", + }, + Config: &Config{}, + TransformTemplate: "{{}", + }, + DPOP: &DPOPOptions{ + SigningKey: key, + }, + }, + expectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC transformation template: template: transform:1: unexpected "}" in command`), + }, + { + name: "fail/no-dpop-options", + fields: fields{ + OIDC: &OIDCOptions{ + Provider: &Provider{ + IssuerURL: "https://example.com", + }, + Config: &Config{}, + }, + DPOP: nil, + }, + expectedErr: errors.New("no DPoP options available"), + }, + { + name: "fail/invalid-key", + fields: fields{ + OIDC: &OIDCOptions{ + Provider: &Provider{ + IssuerURL: "https://example.com", + }, + Config: &Config{}, + }, + DPOP: &DPOPOptions{ + SigningKey: []byte{0x00}, + Target: "", + }, + }, + expectedErr: errors.New(`failed initializing DPoP options: failed parsing key: error decoding PEM: not a valid PEM encoded block`), + }, + { + name: "fail/target-template", + fields: fields{ + OIDC: &OIDCOptions{ + Provider: &Provider{ + IssuerURL: "https://example.com", + }, + Config: &Config{}, + }, + DPOP: &DPOPOptions{ + SigningKey: key, + Target: "{{}", + }, + }, + expectedErr: errors.New(`failed initializing DPoP options: failed parsing DPoP template: template: DeviceID:1: unexpected "}" in command`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &Options{ + OIDC: tt.fields.OIDC, + DPOP: tt.fields.DPOP, + } + err := o.Validate() + if tt.expectedErr != nil { + assert.EqualError(t, err, tt.expectedErr.Error()) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/go.mod b/go.mod index 19a0d752..8419abdc 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( cloud.google.com/go/longrunning v0.5.6 cloud.google.com/go/security v1.16.0 github.com/Masterminds/sprig/v3 v3.2.3 + github.com/coreos/go-oidc/v3 v3.4.0 github.com/dgraph-io/badger v1.6.2 github.com/dgraph-io/badger/v2 v2.2007.4 github.com/fxamacker/cbor/v2 v2.6.0 @@ -164,5 +165,6 @@ require ( google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9f8ef365..f7ed72e7 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,75 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= cloud.google.com/go/auth v0.2.2 h1:gmxNJs4YZYcw6YvKRtVBaF2fyUE6UrWPyzU8jHvYfmI= cloud.google.com/go/auth v0.2.2/go.mod h1:2bDNJWtWziDT3Pu1URxHHbkHE/BbOCuyUiKIGcNvafo= cloud.google.com/go/auth/oauth2adapt v0.2.1 h1:VSPmMmUlT8CkIZ2PzD9AlLN+R3+D1clXMWHHa6vG/Ag= cloud.google.com/go/auth/oauth2adapt v0.2.1/go.mod h1:tOdK/k+D2e4GEwfBRA48dKNQiDsqIXxLh7VU319eV0g= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs= cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs= cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/security v1.16.0 h1:dzc3oYxFG/9+uMwmxpnG+te4ZEEkbzvBoAFR1Va36N4= cloud.google.com/go/security v1.16.0/go.mod h1:e1GsICfB1nLCRXOq0yaRlKE/6RUAlBqmalTYQH4J2Xo= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= @@ -33,6 +88,7 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -44,6 +100,7 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E= github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= @@ -96,10 +153,20 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc/v3 v3.4.0 h1:xz7elHb/LDwm/ERpwHd+5nb7wFHL32rsr6bBOgaeu6g= +github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -129,6 +196,12 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -138,8 +211,12 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= @@ -175,27 +252,44 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= @@ -206,9 +300,16 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -226,6 +327,24 @@ github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= @@ -233,10 +352,20 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -261,6 +390,8 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/vault/api v1.12.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck= @@ -272,6 +403,8 @@ github.com/hashicorp/vault/api/auth/kubernetes v0.6.0 h1:K8sKGhtTAqGKfzaaYvUSIOA github.com/hashicorp/vault/api/auth/kubernetes v0.6.0/go.mod h1:Htwcjez5J9PwAHaZ1EYMBlgGq3/in5ajUV4+WCPihPE= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -323,6 +456,8 @@ github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= @@ -407,6 +542,7 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -482,11 +618,22 @@ github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOH github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= @@ -501,6 +648,7 @@ go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ= go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8= go.step.sm/crypto v0.44.6 h1:vQg8ujce7fNXDO8EWdriSz+ZSJpYnNh22QrFtRjdyoY= @@ -524,6 +672,7 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -539,28 +688,89 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -568,12 +778,40 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= @@ -585,24 +823,73 @@ golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -625,10 +912,13 @@ golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -637,6 +927,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -644,15 +937,58 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -661,13 +997,135 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= google.golang.org/api v0.176.0 h1:dHj1/yv5Dm/eQTXiP9hNCRT3xzJHWXeNdRq29XbMxoE= google.golang.org/api v0.176.0/go.mod h1:Rra+ltKu14pps/4xTycZfobMgLpbosoaaL7c+SEMrO8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38= @@ -675,12 +1133,39 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -689,7 +1174,12 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -699,7 +1189,10 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -707,5 +1200,12 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=