Initial project snapshot
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
package authority
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/config"
|
||||
postgresplatform "github.com/example/remote-access-platform/backend/internal/platform/postgres"
|
||||
)
|
||||
|
||||
const (
|
||||
ModeStrict = "strict"
|
||||
ModeLegacy = "legacy"
|
||||
|
||||
ActivationSchemaVersion = "rap.installation.activation.v1"
|
||||
|
||||
PlatformRoleUser = "user"
|
||||
PlatformRoleAdmin = "platform_admin"
|
||||
PlatformRoleRecoveryAdmin = "platform_recovery_admin"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidAuthorityMode = errors.New("invalid installation authority mode")
|
||||
ErrProductRootKeyNeeded = errors.New("product root public key is required")
|
||||
ErrInvalidActivation = errors.New("invalid installation activation")
|
||||
ErrInvalidGrant = errors.New("invalid platform role grant")
|
||||
)
|
||||
|
||||
type ActivationPayload struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
InstallID string `json:"install_id"`
|
||||
OwnerEmail string `json:"owner_email"`
|
||||
PlatformRole string `json:"platform_role"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Environment string `json:"environment,omitempty"`
|
||||
}
|
||||
|
||||
type Verifier struct {
|
||||
mode string
|
||||
rootPublicKey ed25519.PublicKey
|
||||
rootFingerprint string
|
||||
allowInsecureBootstrap bool
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewVerifier(cfg config.InstallationConfig) (*Verifier, error) {
|
||||
mode := strings.ToLower(strings.TrimSpace(cfg.AuthorityMode))
|
||||
if mode == "" {
|
||||
mode = ModeLegacy
|
||||
}
|
||||
verifier := &Verifier{
|
||||
mode: mode,
|
||||
allowInsecureBootstrap: cfg.AllowInsecureBootstrap,
|
||||
now: time.Now,
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case ModeLegacy:
|
||||
return verifier, nil
|
||||
case ModeStrict:
|
||||
publicKey, err := decodeEd25519PublicKey(cfg.ProductRootPublicKeyBase64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verifier.rootPublicKey = publicKey
|
||||
fingerprint := sha256.Sum256(publicKey)
|
||||
verifier.rootFingerprint = hex.EncodeToString(fingerprint[:])
|
||||
return verifier, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidAuthorityMode, mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Verifier) Mode() string {
|
||||
if v == nil || v.mode == "" {
|
||||
return ModeLegacy
|
||||
}
|
||||
return v.mode
|
||||
}
|
||||
|
||||
func (v *Verifier) Strict() bool {
|
||||
return v != nil && v.mode == ModeStrict
|
||||
}
|
||||
|
||||
func (v *Verifier) AllowInsecureBootstrap() bool {
|
||||
return v != nil && v.allowInsecureBootstrap
|
||||
}
|
||||
|
||||
func (v *Verifier) RootFingerprint() string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return v.rootFingerprint
|
||||
}
|
||||
|
||||
func (v *Verifier) VerifyActivation(payload json.RawMessage, signature string) (ActivationPayload, error) {
|
||||
if v == nil || !v.Strict() {
|
||||
return ActivationPayload{}, ErrProductRootKeyNeeded
|
||||
}
|
||||
activation, canonical, err := parseActivationPayload(payload)
|
||||
if err != nil {
|
||||
return ActivationPayload{}, err
|
||||
}
|
||||
if err := activation.validate(v.now().UTC()); err != nil {
|
||||
return ActivationPayload{}, err
|
||||
}
|
||||
if err := v.verifySignature(canonical, signature); err != nil {
|
||||
return ActivationPayload{}, fmt.Errorf("%w: %v", ErrInvalidActivation, err)
|
||||
}
|
||||
return activation, nil
|
||||
}
|
||||
|
||||
func (v *Verifier) VerifyPlatformRoleGrant(payload json.RawMessage, signature, expectedInstallID, expectedEmail, expectedRole string) (ActivationPayload, error) {
|
||||
activation, err := v.VerifyActivation(payload, signature)
|
||||
if err != nil {
|
||||
return ActivationPayload{}, fmt.Errorf("%w: %v", ErrInvalidGrant, err)
|
||||
}
|
||||
if activation.InstallID != strings.TrimSpace(expectedInstallID) {
|
||||
return ActivationPayload{}, fmt.Errorf("%w: install_id mismatch", ErrInvalidGrant)
|
||||
}
|
||||
if !strings.EqualFold(activation.OwnerEmail, strings.TrimSpace(expectedEmail)) {
|
||||
return ActivationPayload{}, fmt.Errorf("%w: owner_email mismatch", ErrInvalidGrant)
|
||||
}
|
||||
if activation.PlatformRole != strings.TrimSpace(expectedRole) {
|
||||
return ActivationPayload{}, fmt.Errorf("%w: platform_role mismatch", ErrInvalidGrant)
|
||||
}
|
||||
return activation, nil
|
||||
}
|
||||
|
||||
func CanonicalJSON(raw json.RawMessage) ([]byte, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, fmt.Errorf("%w: empty payload", ErrInvalidActivation)
|
||||
}
|
||||
var value any
|
||||
if err := json.Unmarshal(raw, &value); err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid json: %v", ErrInvalidActivation, err)
|
||||
}
|
||||
canonical, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: canonical json: %v", ErrInvalidActivation, err)
|
||||
}
|
||||
return canonical, nil
|
||||
}
|
||||
|
||||
func EffectivePlatformRole(ctx context.Context, db postgresplatform.DBTX, verifier *Verifier, userID string) (string, error) {
|
||||
userID = strings.TrimSpace(userID)
|
||||
if userID == "" {
|
||||
return PlatformRoleUser, nil
|
||||
}
|
||||
if verifier == nil || !verifier.Strict() {
|
||||
return legacyPlatformRole(ctx, db, userID)
|
||||
}
|
||||
|
||||
var email string
|
||||
if err := db.QueryRow(ctx, `SELECT email FROM users WHERE id = $1::uuid`, userID).Scan(&email); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return PlatformRoleUser, nil
|
||||
}
|
||||
return "", fmt.Errorf("get user email for platform grant: %w", err)
|
||||
}
|
||||
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT prg.role, prg.install_id, prg.grant_payload, prg.grant_signature
|
||||
FROM platform_role_grants prg
|
||||
JOIN installation_authority ia
|
||||
ON ia.id = 1
|
||||
AND ia.install_id = prg.install_id
|
||||
AND ia.authority_state = 'active'
|
||||
WHERE prg.user_id = $1::uuid
|
||||
AND prg.revoked_at IS NULL
|
||||
AND (prg.expires_at IS NULL OR prg.expires_at > NOW())
|
||||
ORDER BY CASE prg.role
|
||||
WHEN 'platform_recovery_admin' THEN 0
|
||||
WHEN 'platform_admin' THEN 1
|
||||
ELSE 2
|
||||
END, prg.granted_at DESC
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("query platform role grants: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
bestRole := PlatformRoleUser
|
||||
for rows.Next() {
|
||||
var role, installID, signature string
|
||||
var payload []byte
|
||||
if err := rows.Scan(&role, &installID, &payload, &signature); err != nil {
|
||||
return "", fmt.Errorf("scan platform role grant: %w", err)
|
||||
}
|
||||
if _, err := verifier.VerifyPlatformRoleGrant(json.RawMessage(payload), signature, installID, email, role); err != nil {
|
||||
continue
|
||||
}
|
||||
if role == PlatformRoleRecoveryAdmin {
|
||||
return role, nil
|
||||
}
|
||||
if role == PlatformRoleAdmin {
|
||||
bestRole = role
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return "", fmt.Errorf("iterate platform role grants: %w", err)
|
||||
}
|
||||
return bestRole, nil
|
||||
}
|
||||
|
||||
func legacyPlatformRole(ctx context.Context, db postgresplatform.DBTX, userID string) (string, error) {
|
||||
var role string
|
||||
if err := db.QueryRow(ctx, `SELECT platform_role FROM users WHERE id = $1::uuid`, userID).Scan(&role); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return PlatformRoleUser, nil
|
||||
}
|
||||
return "", fmt.Errorf("get platform role: %w", err)
|
||||
}
|
||||
if role == "" {
|
||||
return PlatformRoleUser, nil
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func parseActivationPayload(raw json.RawMessage) (ActivationPayload, []byte, error) {
|
||||
canonical, err := CanonicalJSON(raw)
|
||||
if err != nil {
|
||||
return ActivationPayload{}, nil, err
|
||||
}
|
||||
var activation ActivationPayload
|
||||
if err := json.Unmarshal(canonical, &activation); err != nil {
|
||||
return ActivationPayload{}, nil, fmt.Errorf("%w: decode activation: %v", ErrInvalidActivation, err)
|
||||
}
|
||||
activation.SchemaVersion = strings.TrimSpace(activation.SchemaVersion)
|
||||
activation.InstallID = strings.TrimSpace(activation.InstallID)
|
||||
activation.OwnerEmail = strings.ToLower(strings.TrimSpace(activation.OwnerEmail))
|
||||
activation.PlatformRole = strings.TrimSpace(activation.PlatformRole)
|
||||
activation.Nonce = strings.TrimSpace(activation.Nonce)
|
||||
activation.Environment = strings.TrimSpace(activation.Environment)
|
||||
return activation, canonical, nil
|
||||
}
|
||||
|
||||
func (p ActivationPayload) validate(now time.Time) error {
|
||||
if p.SchemaVersion != ActivationSchemaVersion {
|
||||
return fmt.Errorf("%w: schema_version must be %s", ErrInvalidActivation, ActivationSchemaVersion)
|
||||
}
|
||||
if p.InstallID == "" {
|
||||
return fmt.Errorf("%w: install_id is required", ErrInvalidActivation)
|
||||
}
|
||||
if p.OwnerEmail == "" || !strings.Contains(p.OwnerEmail, "@") {
|
||||
return fmt.Errorf("%w: owner_email is required", ErrInvalidActivation)
|
||||
}
|
||||
switch p.PlatformRole {
|
||||
case PlatformRoleAdmin, PlatformRoleRecoveryAdmin:
|
||||
default:
|
||||
return fmt.Errorf("%w: platform_role must be platform_admin or platform_recovery_admin", ErrInvalidActivation)
|
||||
}
|
||||
if p.IssuedAt.IsZero() {
|
||||
return fmt.Errorf("%w: issued_at is required", ErrInvalidActivation)
|
||||
}
|
||||
if p.IssuedAt.After(now.Add(5 * time.Minute)) {
|
||||
return fmt.Errorf("%w: issued_at is too far in the future", ErrInvalidActivation)
|
||||
}
|
||||
if p.ExpiresAt != nil && !p.ExpiresAt.After(now) {
|
||||
return fmt.Errorf("%w: activation expired", ErrInvalidActivation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Verifier) verifySignature(payload []byte, signatureText string) error {
|
||||
signature, err := decodeBase64(strings.TrimSpace(signatureText))
|
||||
if err != nil {
|
||||
return fmt.Errorf("signature must be base64 encoded: %w", err)
|
||||
}
|
||||
if len(signature) != ed25519.SignatureSize {
|
||||
return fmt.Errorf("signature must decode to %d bytes", ed25519.SignatureSize)
|
||||
}
|
||||
if !ed25519.Verify(v.rootPublicKey, payload, signature) {
|
||||
return errors.New("signature verification failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeEd25519PublicKey(value string) (ed25519.PublicKey, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, ErrProductRootKeyNeeded
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(value)); block != nil {
|
||||
parsed, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse product root public key PEM: %w", err)
|
||||
}
|
||||
publicKey, ok := parsed.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("product root public key PEM must contain an Ed25519 public key")
|
||||
}
|
||||
return publicKey, nil
|
||||
}
|
||||
decoded, err := decodeBase64(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product root public key must be base64 encoded: %w", err)
|
||||
}
|
||||
if len(decoded) != ed25519.PublicKeySize {
|
||||
return nil, fmt.Errorf("product root public key must decode to %d bytes", ed25519.PublicKeySize)
|
||||
}
|
||||
return ed25519.PublicKey(decoded), nil
|
||||
}
|
||||
|
||||
func decodeBase64(value string) ([]byte, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(value)
|
||||
if err == nil {
|
||||
return decoded, nil
|
||||
}
|
||||
decoded, rawErr := base64.RawStdEncoding.DecodeString(value)
|
||||
if rawErr == nil {
|
||||
return decoded, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
Reference in New Issue
Block a user