Refactor RDP proxy handling and update related tests

This commit is contained in:
2026-05-17 20:38:35 +03:00
parent 8e9402580f
commit d551e57fd5
172 changed files with 22117 additions and 2509 deletions
@@ -16,6 +16,8 @@ import (
const (
AuthoritySchemaVersion = "rap.cluster_authority.v1"
SignatureSchemaVersion = "rap.cluster_authority.signature.v1"
QuorumSchemaVersion = "rap.cluster_authority.quorum.v1"
QuorumEnvelopeVersion = "rap.cluster_authority.quorum_envelope.v1"
AlgorithmEd25519 = "ed25519"
)
@@ -39,6 +41,34 @@ type Signature struct {
SignedAt time.Time `json:"signed_at"`
}
type QuorumMember struct {
NodeID string `json:"node_id,omitempty"`
Role string `json:"role,omitempty"`
PublicKey string `json:"public_key"`
PublicKeyFingerprint string `json:"public_key_fingerprint"`
Scopes []string `json:"scopes,omitempty"`
}
type QuorumDescriptor struct {
SchemaVersion string `json:"schema_version"`
ClusterID string `json:"cluster_id"`
Epoch string `json:"epoch"`
Threshold int `json:"threshold"`
Members []QuorumMember `json:"members"`
}
type QuorumEnvelope struct {
SchemaVersion string `json:"schema_version"`
ClusterID string `json:"cluster_id"`
Epoch string `json:"epoch"`
Threshold int `json:"threshold"`
PayloadSHA256 string `json:"payload_sha256"`
QuorumSHA256 string `json:"quorum_sha256"`
Signatures []Signature `json:"signatures"`
AllowedScopes []string `json:"allowed_scopes,omitempty"`
DecisionReason string `json:"decision_reason,omitempty"`
}
func GenerateKeyPair() (KeyPair, error) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
@@ -128,6 +158,82 @@ func VerifyRaw(publicKeyB64 string, payload json.RawMessage, signature Signature
return nil
}
func VerifyQuorumRaw(descriptor QuorumDescriptor, payload json.RawMessage, envelope QuorumEnvelope, requiredScope string) error {
if descriptor.SchemaVersion != QuorumSchemaVersion {
return fmt.Errorf("%w: quorum schema_version must be %s", ErrInvalidSignature, QuorumSchemaVersion)
}
if envelope.SchemaVersion != QuorumEnvelopeVersion {
return fmt.Errorf("%w: quorum envelope schema_version must be %s", ErrInvalidSignature, QuorumEnvelopeVersion)
}
if strings.TrimSpace(descriptor.ClusterID) == "" || descriptor.ClusterID != envelope.ClusterID {
return fmt.Errorf("%w: quorum cluster mismatch", ErrInvalidSignature)
}
if strings.TrimSpace(descriptor.Epoch) == "" || descriptor.Epoch != envelope.Epoch {
return fmt.Errorf("%w: quorum epoch mismatch", ErrInvalidSignature)
}
threshold := descriptor.Threshold
if envelope.Threshold > threshold {
threshold = envelope.Threshold
}
if threshold <= 0 || threshold > len(descriptor.Members) {
return fmt.Errorf("%w: invalid quorum threshold", ErrInvalidSignature)
}
payloadHash, err := HashRaw(payload)
if err != nil {
return err
}
if envelope.PayloadSHA256 != payloadHash {
return fmt.Errorf("%w: quorum payload hash mismatch", ErrInvalidSignature)
}
descriptorHash, err := QuorumDescriptorHash(descriptor)
if err != nil {
return err
}
if envelope.QuorumSHA256 != descriptorHash {
return fmt.Errorf("%w: quorum descriptor hash mismatch", ErrInvalidSignature)
}
members := map[string]QuorumMember{}
for _, member := range descriptor.Members {
fingerprint := strings.TrimSpace(member.PublicKeyFingerprint)
if fingerprint == "" {
publicKey, err := DecodePublicKey(member.PublicKey)
if err != nil {
return err
}
fingerprint = Fingerprint(publicKey)
}
if _, exists := members[fingerprint]; exists {
return fmt.Errorf("%w: duplicate quorum member", ErrInvalidSignature)
}
member.PublicKeyFingerprint = fingerprint
members[fingerprint] = member
}
seen := map[string]bool{}
valid := 0
for _, signature := range envelope.Signatures {
fingerprint := strings.TrimSpace(signature.KeyFingerprint)
if seen[fingerprint] {
continue
}
member, ok := members[fingerprint]
if !ok {
return fmt.Errorf("%w: quorum signer is not a member", ErrInvalidSignature)
}
if requiredScope != "" && !memberAllowsScope(member, requiredScope) {
return fmt.Errorf("%w: quorum signer scope mismatch", ErrInvalidSignature)
}
if err := VerifyRaw(member.PublicKey, payload, signature); err != nil {
return err
}
seen[fingerprint] = true
valid++
}
if valid < threshold {
return fmt.Errorf("%w: quorum threshold not met", ErrInvalidSignature)
}
return nil
}
func CanonicalJSON(raw json.RawMessage) ([]byte, error) {
if len(raw) == 0 {
return nil, fmt.Errorf("%w: empty payload", ErrInvalidPayload)
@@ -152,6 +258,28 @@ func HashRaw(raw json.RawMessage) (string, error) {
return hex.EncodeToString(sum[:]), nil
}
func QuorumDescriptorHash(descriptor QuorumDescriptor) (string, error) {
raw, err := json.Marshal(descriptor)
if err != nil {
return "", err
}
return HashRaw(raw)
}
func memberAllowsScope(member QuorumMember, requiredScope string) bool {
requiredScope = strings.TrimSpace(requiredScope)
if requiredScope == "" {
return true
}
for _, scope := range member.Scopes {
scope = strings.TrimSpace(scope)
if scope == "*" || scope == requiredScope {
return true
}
}
return false
}
func DecodePublicKey(value string) (ed25519.PublicKey, error) {
decoded, err := decodeBase64(strings.TrimSpace(value))
if err != nil {