Refactor RDP proxy handling and update related tests
This commit is contained in:
@@ -14,6 +14,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"
|
||||
)
|
||||
|
||||
@@ -30,6 +32,34 @@ type Signature struct {
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
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 VerifyRaw(publicKeyB64 string, payload json.RawMessage, signature Signature) error {
|
||||
if signature.SchemaVersion != SignatureSchemaVersion {
|
||||
return fmt.Errorf("%w: schema_version must be %s", ErrInvalidSignature, SignatureSchemaVersion)
|
||||
@@ -58,6 +88,86 @@ 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 := HashRaw(mustMarshalQuorumDescriptor(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 QuorumDescriptorHash(descriptor QuorumDescriptor) (string, error) {
|
||||
return HashRaw(mustMarshalQuorumDescriptor(descriptor))
|
||||
}
|
||||
|
||||
func Fingerprint(publicKey ed25519.PublicKey) string {
|
||||
sum := sha256.Sum256(publicKey)
|
||||
return "rap-ca-ed25519-" + hex.EncodeToString(sum[:16])
|
||||
@@ -72,6 +182,28 @@ func HashRaw(raw json.RawMessage) (string, error) {
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func mustMarshalQuorumDescriptor(descriptor QuorumDescriptor) json.RawMessage {
|
||||
raw, err := json.Marshal(descriptor)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return 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 CanonicalJSON(raw json.RawMessage) ([]byte, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, fmt.Errorf("%w: empty payload", ErrInvalidPayload)
|
||||
|
||||
Reference in New Issue
Block a user