Files
shankar0123 21aeed4f4e legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):

addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:

  // Copyright 2026 certctl LLC. All rights reserved.
  // SPDX-License-Identifier: BUSL-1.1

Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).

Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.

Generated via:
  addlicense -c "certctl LLC" -y 2026 \
    -f cowork/legal/copyright-header.tpl \
    -ignore '**/testdata/**' -ignore '**/*_test.go' \
    cmd/ internal/

Verification:
  find cmd internal -name '*.go' -not -name '*_test.go' \
    -not -path '*/testdata/*' \
    -exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l

  Returns: 0

gofmt clean. Header additions are comments only, no compile impact.

Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
2026-05-13 21:23:35 +00:00

206 lines
6.3 KiB
Go

// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
package azurekv
// sdk_client.go isolates the imports of github.com/Azure/azure-sdk-for-go/
// sdk/azidentity + sdk/security/keyvault/azcertificates so that
// NewWithClient (the test path) compiles without dragging the SDK
// transitive deps into test binaries.
//
// The production New() path is the only caller of buildSDKClient.
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates"
)
// sdkClient is the production KeyVaultClient implementation backed by
// *azcertificates.Client. Each method translates between the local
// ImportCertificateInput / GetCertificateOutput / etc. shapes and the
// SDK-typed equivalents.
type sdkClient struct {
client *azcertificates.Client
}
// buildSDKClient constructs an *azcertificates.Client wrapped in
// sdkClient. The credential chain is selected by credMode:
//
// "" / "default" — DefaultAzureCredential
// "managed_identity" — ManagedIdentityCredential
// "client_secret" — ClientSecretCredential (env vars only)
// "workload_identity" — WorkloadIdentityCredential
//
// Any error from credential construction or client init bubbles up
// to the caller (typically ValidateConfig or New).
func buildSDKClient(ctx context.Context, vaultURL, credMode string) (KeyVaultClient, error) {
cred, err := buildCredential(credMode)
if err != nil {
return nil, fmt.Errorf("Azure credential init: %w", err)
}
clientOpts := &azcertificates.ClientOptions{
ClientOptions: azcore.ClientOptions{
Transport: &http.Client{Timeout: 30 * time.Second},
Retry: policy.RetryOptions{
MaxRetries: 3,
},
},
}
client, err := azcertificates.NewClient(vaultURL, cred, clientOpts)
if err != nil {
return nil, fmt.Errorf("azcertificates.NewClient: %w", err)
}
return &sdkClient{client: client}, nil
}
func buildCredential(credMode string) (azcore.TokenCredential, error) {
switch credMode {
case "", CredModeDefault:
return azidentity.NewDefaultAzureCredential(nil)
case CredModeManagedIdentity:
return azidentity.NewManagedIdentityCredential(nil)
case CredModeClientSecret:
return azidentity.NewEnvironmentCredential(nil)
case CredModeWorkloadIdentity:
return azidentity.NewWorkloadIdentityCredential(nil)
default:
return nil, fmt.Errorf("unsupported credential_mode %q", credMode)
}
}
func (s *sdkClient) ImportCertificate(ctx context.Context, in *ImportCertificateInput) (*ImportCertificateOutput, error) {
tagsPtr := make(map[string]*string, len(in.Tags))
for k, v := range in.Tags {
v := v // capture
tagsPtr[k] = &v
}
resp, err := s.client.ImportCertificate(ctx, in.CertificateName, azcertificates.ImportCertificateParameters{
Base64EncodedCertificate: ptrTo(in.PFXBase64),
Tags: tagsPtr,
}, nil)
if err != nil {
return nil, fmt.Errorf("azcertificates ImportCertificate: %w", err)
}
out := &ImportCertificateOutput{}
if resp.ID != nil {
out.KID = string(*resp.ID)
// Version ID is the last path segment: .../certificates/<name>/<version>.
out.VersionID = lastPathSegment(out.KID)
}
return out, nil
}
func (s *sdkClient) GetCertificate(ctx context.Context, in *GetCertificateInput) (*GetCertificateOutput, error) {
resp, err := s.client.GetCertificate(ctx, in.CertificateName, in.Version, nil)
if err != nil {
return nil, fmt.Errorf("azcertificates GetCertificate: %w", err)
}
out := &GetCertificateOutput{
CERBytes: resp.CER,
}
if resp.ID != nil {
out.VersionID = lastPathSegment(string(*resp.ID))
}
if resp.Attributes != nil {
if resp.Attributes.NotBefore != nil {
out.NotBefore = *resp.Attributes.NotBefore
}
if resp.Attributes.Expires != nil {
out.NotAfter = *resp.Attributes.Expires
}
}
// Parse serial from the CER bytes; Key Vault doesn't expose it
// directly on the response struct.
if len(resp.CER) > 0 {
if cert, parseErr := x509.ParseCertificate(resp.CER); parseErr == nil {
out.Serial = serialFromX509(cert)
}
}
// X509Thumbprint is also available; we use Serial for parity with
// the AWS ACM connector's verify path.
return out, nil
}
func (s *sdkClient) ListVersions(ctx context.Context, in *ListVersionsInput) (*ListVersionsOutput, error) {
out := &ListVersionsOutput{}
pager := s.client.NewListCertificatePropertiesVersionsPager(in.CertificateName, nil)
max := in.MaxItems
if max == 0 {
max = 100
}
for pager.More() && int32(len(out.Versions)) < max {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("azcertificates ListVersions: %w", err)
}
for _, v := range page.Value {
vs := VersionSummary{}
if v.ID != nil {
vs.VersionID = lastPathSegment(string(*v.ID))
}
if v.Attributes != nil {
if v.Attributes.NotBefore != nil {
vs.NotBefore = *v.Attributes.NotBefore
}
if v.Attributes.Enabled != nil {
vs.Enabled = *v.Attributes.Enabled
}
}
out.Versions = append(out.Versions, vs)
if int32(len(out.Versions)) >= max {
break
}
}
}
return out, nil
}
// ptrTo is a helper for the SDK's heavy use of *T parameters.
func ptrTo[T any](v T) *T { return &v }
// lastPathSegment returns everything after the final '/' in a URI.
// Used to extract the Key Vault version ID from a cert KID.
func lastPathSegment(uri string) string {
for i := len(uri) - 1; i >= 0; i-- {
if uri[i] == '/' {
return uri[i+1:]
}
}
return uri
}
// serialFromX509 formats an x509.Certificate's SerialNumber to match
// the colon-separated lowercase-hex shape the Azure SDK emits + the
// AWS ACM connector uses for cross-cloud parity.
func serialFromX509(cert *x509.Certificate) string {
hex := fmt.Sprintf("%x", cert.SerialNumber)
if len(hex)%2 == 1 {
hex = "0" + hex
}
out := make([]byte, 0, len(hex)+(len(hex)/2)-1)
for i := 0; i < len(hex); i += 2 {
if i > 0 {
out = append(out, ':')
}
out = append(out, hex[i], hex[i+1])
}
return string(out)
}
// Compile-time assertion: *sdkClient implements KeyVaultClient.
var _ KeyVaultClient = (*sdkClient)(nil)
// _ = pem keeps the import stable across refactors that drop and
// re-add PEM-handling code paths.
var _ = pem.Decode