registry/cmd/publisher/auth/common.go
Radoslav Dimitrov 4805f5c66d
Fix main by implementing a go1.24 compliant parseRawPrivateKey method (#707)
<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
The following fixes the currently failing build on main because we
reverted to go 1.24 and the last commit was relying on a method solely
available in 1.25.

The method is quite straightforward so we just reimplemented it. 

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
2025-10-24 23:07:55 +03:00

200 lines
5.6 KiB
Go

package auth
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"time"
)
type CryptoAlgorithm string
const (
AlgorithmEd25519 CryptoAlgorithm = "ed25519"
// ECDSA with NIST P-384 curve
// public key is in compressed format
// signature is in R || S format
AlgorithmECDSAP384 CryptoAlgorithm = "ecdsap384"
)
// CryptoProvider provides common functionality for DNS and HTTP authentication
type CryptoProvider struct {
registryURL string
domain string
privateKey string
cryptoAlgorithm CryptoAlgorithm
authMethod string
}
// GetToken retrieves the registry JWT token using cryptographic authentication
func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
if c.domain == "" {
return "", fmt.Errorf("%s domain is required", c.authMethod)
}
if c.privateKey == "" {
return "", fmt.Errorf("%s private key (hex) is required", c.authMethod)
}
// Decode private key from hex
privateKeyBytes, err := hex.DecodeString(c.privateKey)
if err != nil {
return "", fmt.Errorf("invalid hex private key format: %w", err)
}
// Generate current timestamp
timestamp := time.Now().UTC().Format(time.RFC3339)
signedTimestamp, err := c.signMessage(privateKeyBytes, []byte(timestamp))
if err != nil {
return "", fmt.Errorf("failed to sign timestamp: %w", err)
}
signedTimestampHex := hex.EncodeToString(signedTimestamp)
// Exchange signature for registry token
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, timestamp, signedTimestampHex)
if err != nil {
return "", fmt.Errorf("failed to exchange %s signature: %w", c.authMethod, err)
}
return registryToken, nil
}
func (c *CryptoProvider) signMessage(privateKeyBytes []byte, message []byte) ([]byte, error) {
switch c.cryptoAlgorithm {
case AlgorithmEd25519:
if len(privateKeyBytes) != ed25519.SeedSize {
return nil, fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(privateKeyBytes))
}
privateKey := ed25519.NewKeyFromSeed(privateKeyBytes)
signature := ed25519.Sign(privateKey, message)
return signature, nil
case AlgorithmECDSAP384:
if len(privateKeyBytes) != 48 {
return nil, fmt.Errorf("invalid seed length for ECDSA P-384: expected 48 bytes, got %d", len(privateKeyBytes))
}
digest := sha512.Sum384(message)
curve := elliptic.P384()
// Parse the raw private key (compatible with Go 1.24)
privateKey, err := parseRawPrivateKey(curve, privateKeyBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
}
r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:])
if err != nil {
return nil, fmt.Errorf("failed to sign message: %w", err)
}
signature := append(r.Bytes(), s.Bytes()...)
return signature, nil
default:
return nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
}
}
// parseRawPrivateKey parses a raw ECDSA private key from bytes.
// This mimics crypto/ecdsa.ParseRawPrivateKey from Go 1.25+ for compatibility with Go 1.24.
func parseRawPrivateKey(curve elliptic.Curve, privateKeyBytes []byte) (*ecdsa.PrivateKey, error) {
if curve == nil {
return nil, fmt.Errorf("nil curve")
}
// Only standard NIST curves supported
switch curve {
case elliptic.P224(), elliptic.P256(), elliptic.P384(), elliptic.P521():
// ok
default:
return nil, fmt.Errorf("unsupported curve")
}
d := new(big.Int).SetBytes(privateKeyBytes)
params := curve.Params()
if d.Sign() <= 0 || d.Cmp(params.N) >= 0 {
return nil, fmt.Errorf("invalid private scalar")
}
x, y := curve.ScalarBaseMult(d.Bytes())
return &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: curve,
X: x,
Y: y,
},
D: d,
}, nil
}
// NeedsLogin always returns false for cryptographic auth since no interactive login is needed
func (c *CryptoProvider) NeedsLogin() bool {
return false
}
// Login is not needed for cryptographic auth since authentication is cryptographic
func (c *CryptoProvider) Login(_ context.Context) error {
return nil
}
// exchangeTokenForRegistry exchanges signature for a registry JWT token
func (c *CryptoProvider) exchangeTokenForRegistry(ctx context.Context, domain, timestamp, signedTimestamp string) (string, error) {
if c.registryURL == "" {
return "", fmt.Errorf("registry URL is required for token exchange")
}
// Prepare the request body
payload := map[string]string{
"domain": domain,
"timestamp": timestamp,
"signed_timestamp": signedTimestamp,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// Make the token exchange request
exchangeURL := fmt.Sprintf("%s/v0/auth/%s", c.registryURL, c.authMethod)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, body)
}
var tokenResp RegistryTokenResponse
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return "", fmt.Errorf("failed to unmarshal response: %w", err)
}
return tokenResp.RegistryToken, nil
}