diff --git a/codec/codec.go b/codec/codec.go index bfc80c6..ad3b95d 100644 --- a/codec/codec.go +++ b/codec/codec.go @@ -4,6 +4,8 @@ import ( gcpKms "cloud.google.com/go/kms/apiv1" "context" "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" awsKms "github.com/aws/aws-sdk-go/service/kms" @@ -136,9 +138,86 @@ func newCodecFactoryProvider(configProvider config.ConfigProvider) (EncryptionCo ), nil } + cf.providers["azure-keyvault"] = func(args EncryptionCodecOptions) (converter.PayloadCodec, error) { + rawKeyID, ok := args.LocalEncryptionConfig.Config["key-id"] + if !ok { + return nil, fmt.Errorf("key not found in config") + } + keyID, ok := rawKeyID.(string) + if !ok { + return nil, fmt.Errorf("key is not a string") + } + + vaultURL := os.Getenv(config.AzureVaultURLEnvVar) + if vaultURL == "" { + return nil, fmt.Errorf("AZURE_VAULT_URL environment variable is required") + } + + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, fmt.Errorf("failed to create Azure credential: %v", err) + } + + azClient, err := azkeys.NewClient(vaultURL, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create Azure Key Vault client: %v", err) + } + + azAdapter := &azureKeysClientAdapter{client: azClient} + + azureMaterialsManager := crypto.NewAzureKeyVaultProvider(azAdapter, crypto.AzureKeyVaultOptions{ + KeyID: keyID, + Algorithm: "RSA-OAEP-256", + }) + + args.MetricsHandler.AddAttributes(attribute.String("encryption_key", keyID)) + + return NewEncryptionCodecWithCaching( + azureMaterialsManager, + args.CodecContext, + keyID, + args.MetricsHandler, + cf.cachingConfig, + ), nil + } + return cf, nil } +// azureKeysClientAdapter adapts azkeys.Client to the crypto.AzureKeyVaultClient interface +type azureKeysClientAdapter struct { + client *azkeys.Client +} + +func (a *azureKeysClientAdapter) WrapKey(ctx context.Context, keyID string, algorithm string, plaintext []byte) ([]byte, error) { + params := azkeys.KeyOperationParameters{ + Algorithm: toAzKeysAlgorithm(algorithm), + Value: plaintext, + } + resp, err := a.client.WrapKey(ctx, keyID, "", params, nil) + if err != nil { + return nil, err + } + return resp.Result, nil +} + +func (a *azureKeysClientAdapter) UnwrapKey(ctx context.Context, keyID string, algorithm string, ciphertext []byte) ([]byte, error) { + params := azkeys.KeyOperationParameters{ + Algorithm: toAzKeysAlgorithm(algorithm), + Value: ciphertext, + } + resp, err := a.client.UnwrapKey(ctx, keyID, "", params, nil) + if err != nil { + return nil, err + } + return resp.Result, nil +} + +func toAzKeysAlgorithm(algorithm string) *azkeys.EncryptionAlgorithm { + alg := azkeys.EncryptionAlgorithm(algorithm) + return &alg +} + func (e *encryptionCodecFactory) NewEncryptionCodec(args EncryptionCodecOptions) (converter.PayloadCodec, error) { encryptionCodec, ok := e.providers[args.LocalEncryptionConfig.Type] if !ok { diff --git a/config/config.go b/config/config.go index c0b5346..9fa504d 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,8 @@ const ( GcpRegionEnvVar = "GCP_REGION" DefaultGcpRegion = "us-central1" + + AzureVaultURLEnvVar = "AZURE_VAULT_URL" ) type ( diff --git a/crypto/azure_keyvault_provider.go b/crypto/azure_keyvault_provider.go new file mode 100644 index 0000000..d5554f6 --- /dev/null +++ b/crypto/azure_keyvault_provider.go @@ -0,0 +1,79 @@ +package crypto + +import ( + "context" + "crypto/rand" + "fmt" +) + +// AzureKeyVaultOptions contains configuration options for AzureKeyVaultProvider +type AzureKeyVaultOptions struct { + // KeyID is the Azure Key Vault key identifier URL + // Format: https://{vault-name}.vault.azure.net/keys/{key-name}/{key-version} + KeyID string + + // Algorithm is the key wrap algorithm to use (defaults to RSA-OAEP-256 if empty) + Algorithm string +} + +// AzureKeyVaultClient defines the interface for Azure Key Vault key operations +type AzureKeyVaultClient interface { + WrapKey(ctx context.Context, keyID string, algorithm string, plaintext []byte) ([]byte, error) + UnwrapKey(ctx context.Context, keyID string, algorithm string, ciphertext []byte) ([]byte, error) +} + +// AzureKeyVaultProvider implements MaterialsManager using Azure Key Vault +type AzureKeyVaultProvider struct { + client AzureKeyVaultClient + keyID string + algorithm string +} + +// NewAzureKeyVaultProvider creates a new Azure Key Vault-based materials manager +func NewAzureKeyVaultProvider(client AzureKeyVaultClient, options AzureKeyVaultOptions) *AzureKeyVaultProvider { + // Set default algorithm if not provided + algorithm := options.Algorithm + if algorithm == "" { + algorithm = "RSA-OAEP-256" + } + + return &AzureKeyVaultProvider{ + client: client, + keyID: options.KeyID, + algorithm: algorithm, + } +} + +// GetMaterial generates new encryption materials using Azure Key Vault +func (a *AzureKeyVaultProvider) GetMaterial(ctx context.Context, cryptoCtx CryptoContext) (*Material, error) { + // Generate a random 256-bit data key locally + plaintextKey := make([]byte, 32) + if _, err := rand.Read(plaintextKey); err != nil { + return nil, fmt.Errorf("failed to generate random key: %v", err) + } + + // Wrap the data key using Azure Key Vault + encryptedKey, err := a.client.WrapKey(ctx, a.keyID, a.algorithm, plaintextKey) + if err != nil { + return nil, fmt.Errorf("failed to wrap data key: %v", err) + } + + return &Material{ + PlaintextKey: plaintextKey, + EncryptedKey: encryptedKey, + }, nil +} + +// DecryptMaterial decrypts the encrypted key using Azure Key Vault +func (a *AzureKeyVaultProvider) DecryptMaterial(ctx context.Context, cryptoCtx CryptoContext, material *Material) (*Material, error) { + // Unwrap the data key using Azure Key Vault + plaintextKey, err := a.client.UnwrapKey(ctx, a.keyID, a.algorithm, material.EncryptedKey) + if err != nil { + return nil, fmt.Errorf("failed to unwrap data key: %v", err) + } + + return &Material{ + PlaintextKey: plaintextKey, + EncryptedKey: material.EncryptedKey, + }, nil +} diff --git a/crypto/azure_keyvault_provider_test.go b/crypto/azure_keyvault_provider_test.go new file mode 100644 index 0000000..525dd23 --- /dev/null +++ b/crypto/azure_keyvault_provider_test.go @@ -0,0 +1,214 @@ +package crypto + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// MockAzureKeyVaultClient implements AzureKeyVaultClient for testing +type MockAzureKeyVaultClient struct { + wrapKeyOutput []byte + wrapKeyError error + unwrapOutput []byte + unwrapError error + lastKeyID string + lastAlgorithm string + lastPlaintext []byte + lastCipher []byte +} + +func (m *MockAzureKeyVaultClient) WrapKey(ctx context.Context, keyID string, algorithm string, plaintext []byte) ([]byte, error) { + m.lastKeyID = keyID + m.lastAlgorithm = algorithm + m.lastPlaintext = plaintext + return m.wrapKeyOutput, m.wrapKeyError +} + +func (m *MockAzureKeyVaultClient) UnwrapKey(ctx context.Context, keyID string, algorithm string, ciphertext []byte) ([]byte, error) { + m.lastKeyID = keyID + m.lastAlgorithm = algorithm + m.lastCipher = ciphertext + return m.unwrapOutput, m.unwrapError +} + +func TestNewAzureKeyVaultProvider(t *testing.T) { + tests := []struct { + name string + options AzureKeyVaultOptions + expectedAlgorithm string + }{ + { + name: "Default Algorithm", + options: AzureKeyVaultOptions{KeyID: "https://myvault.vault.azure.net/keys/mykey/version1"}, + expectedAlgorithm: "RSA-OAEP-256", + }, + { + name: "Custom Algorithm", + options: AzureKeyVaultOptions{KeyID: "https://myvault.vault.azure.net/keys/mykey/version1", Algorithm: "RSA-OAEP"}, + expectedAlgorithm: "RSA-OAEP", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &MockAzureKeyVaultClient{ + wrapKeyOutput: []byte("test-wrapped-key"), + } + provider := NewAzureKeyVaultProvider(mockClient, tt.options) + + // Verify algorithm by making a call that uses it + cryptoCtx := CryptoContext{"purpose": "test"} + ctx := context.Background() + _, err := provider.GetMaterial(ctx, cryptoCtx) + require.NoError(t, err) + + assert.Equal(t, tt.options.KeyID, mockClient.lastKeyID) + assert.Equal(t, tt.expectedAlgorithm, mockClient.lastAlgorithm) + }) + } +} + +func TestAzureKeyVaultProvider_GetMaterial(t *testing.T) { + tests := []struct { + name string + context CryptoContext + wrapOutput []byte + wrapError error + expectedError bool + }{ + { + name: "Success", + context: CryptoContext{"purpose": "test"}, + wrapOutput: []byte("test-wrapped-key"), + wrapError: nil, + expectedError: false, + }, + { + name: "Wrap Key Error", + context: CryptoContext{"purpose": "test"}, + wrapOutput: nil, + wrapError: errors.New("Key Vault error"), + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &MockAzureKeyVaultClient{ + wrapKeyOutput: tt.wrapOutput, + wrapKeyError: tt.wrapError, + } + + provider := NewAzureKeyVaultProvider(mockClient, AzureKeyVaultOptions{ + KeyID: "https://myvault.vault.azure.net/keys/mykey/version1", + }) + ctx := context.Background() + material, err := provider.GetMaterial(ctx, tt.context) + + if tt.expectedError { + assert.Error(t, err) + assert.Nil(t, material) + } else { + require.NoError(t, err) + assert.NotNil(t, material) + assert.Len(t, material.PlaintextKey, 32) // 256-bit key + assert.Equal(t, tt.wrapOutput, material.EncryptedKey) + + // Verify the plaintext key was sent to WrapKey + assert.Equal(t, material.PlaintextKey, mockClient.lastPlaintext) + assert.Equal(t, "https://myvault.vault.azure.net/keys/mykey/version1", mockClient.lastKeyID) + assert.Equal(t, "RSA-OAEP-256", mockClient.lastAlgorithm) + } + }) + } +} + +func TestAzureKeyVaultProvider_DecryptMaterial(t *testing.T) { + tests := []struct { + name string + context CryptoContext + encryptedKey []byte + unwrapOutput []byte + unwrapError error + expectedPlaintext []byte + expectedError bool + }{ + { + name: "Success", + context: CryptoContext{"purpose": "test"}, + encryptedKey: []byte("test-wrapped-key"), + unwrapOutput: []byte("test-plaintext-key"), + unwrapError: nil, + expectedPlaintext: []byte("test-plaintext-key"), + expectedError: false, + }, + { + name: "Unwrap Key Error", + context: CryptoContext{"purpose": "test"}, + encryptedKey: []byte("test-wrapped-key"), + unwrapOutput: nil, + unwrapError: errors.New("Key Vault error"), + expectedPlaintext: nil, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &MockAzureKeyVaultClient{ + unwrapOutput: tt.unwrapOutput, + unwrapError: tt.unwrapError, + } + + provider := NewAzureKeyVaultProvider(mockClient, AzureKeyVaultOptions{ + KeyID: "https://myvault.vault.azure.net/keys/mykey/version1", + }) + inputMaterial := &Material{ + EncryptedKey: tt.encryptedKey, + } + ctx := context.Background() + material, err := provider.DecryptMaterial(ctx, tt.context, inputMaterial) + + if tt.expectedError { + assert.Error(t, err) + assert.Nil(t, material) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedPlaintext, material.PlaintextKey) + assert.Equal(t, tt.encryptedKey, material.EncryptedKey) + + // Verify the correct ciphertext was sent to UnwrapKey + assert.Equal(t, tt.encryptedKey, mockClient.lastCipher) + assert.Equal(t, "https://myvault.vault.azure.net/keys/mykey/version1", mockClient.lastKeyID) + assert.Equal(t, "RSA-OAEP-256", mockClient.lastAlgorithm) + } + }) + } +} + +func TestAzureKeyVaultProvider_GetMaterialGeneratesUniqueKeys(t *testing.T) { + mockClient := &MockAzureKeyVaultClient{ + wrapKeyOutput: []byte("test-wrapped-key"), + } + + provider := NewAzureKeyVaultProvider(mockClient, AzureKeyVaultOptions{ + KeyID: "https://myvault.vault.azure.net/keys/mykey/version1", + }) + + ctx := context.Background() + cryptoCtx := CryptoContext{"purpose": "test"} + + // Generate two materials and verify they have different plaintext keys + material1, err := provider.GetMaterial(ctx, cryptoCtx) + require.NoError(t, err) + + material2, err := provider.GetMaterial(ctx, cryptoCtx) + require.NoError(t, err) + + assert.NotEqual(t, material1.PlaintextKey, material2.PlaintextKey, + "each call should generate a unique random key") +} diff --git a/go.mod b/go.mod index 7b6c918..1c46e1a 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,14 @@ go 1.24.1 require ( cloud.google.com/go/kms v1.22.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 github.com/MicahParks/keyfunc v1.9.0 github.com/aws/aws-sdk-go v1.55.7 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/googleapis/gax-go/v2 v2.14.2 github.com/hashicorp/golang-lru v1.0.2 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.27.6 go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel/exporters/prometheus v0.59.0 @@ -30,18 +32,25 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/crypto v0.39.0 // indirect + golang.org/x/crypto v0.41.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect ) @@ -75,10 +84,10 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.11.0 // indirect google.golang.org/api v0.232.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect diff --git a/go.sum b/go.sum index 179ffe4..219bc4a 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,22 @@ cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= @@ -56,6 +72,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -86,6 +104,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -105,6 +125,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/nexus-rpc/sdk-go v0.3.0 h1:Y3B0kLYbMhd4C2u00kcYajvmOrfozEtTV/nHSnV57jA= github.com/nexus-rpc/sdk-go v0.3.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -134,8 +156,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= @@ -186,8 +208,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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= @@ -205,8 +227,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= @@ -216,8 +238,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -227,13 +249,14 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=