[DEPRECATED] Go implementation of plcbundle
at rust-test 322 lines 9.0 kB view raw
1package plcclient 2 3import ( 4 "fmt" 5 "strings" 6) 7 8// DIDState represents the current state of a DID (PLC-specific format) 9type DIDState struct { 10 DID string `json:"did"` 11 RotationKeys []string `json:"rotationKeys"` 12 VerificationMethods map[string]string `json:"verificationMethods"` 13 AlsoKnownAs []string `json:"alsoKnownAs"` 14 Services map[string]ServiceDefinition `json:"services"` 15} 16 17// ServiceDefinition represents a service endpoint 18type ServiceDefinition struct { 19 Type string `json:"type"` 20 Endpoint string `json:"endpoint"` 21} 22 23// AuditLogEntry represents a single entry in the audit log 24type AuditLogEntry struct { 25 DID string `json:"did"` 26 Operation interface{} `json:"operation"` // The parsed operation data 27 CID string `json:"cid"` 28 Nullified interface{} `json:"nullified,omitempty"` 29 CreatedAt string `json:"createdAt"` 30} 31 32// ResolveDIDDocument constructs a DID document from operation log 33// This is the main entry point for DID resolution 34func ResolveDIDDocument(did string, operations []PLCOperation) (*DIDDocument, error) { 35 if len(operations) == 0 { 36 return nil, fmt.Errorf("no operations found for DID") 37 } 38 39 // Build current state from operations 40 state, err := BuildDIDState(did, operations) 41 if err != nil { 42 return nil, err 43 } 44 45 // Convert to DID document format 46 return StateToDIDDocument(state), nil 47} 48 49// BuildDIDState applies operations in order to build current DID state 50func BuildDIDState(did string, operations []PLCOperation) (*DIDState, error) { 51 var state *DIDState 52 53 for _, op := range operations { 54 // Skip nullified operations 55 if op.IsNullified() { 56 continue 57 } 58 59 // Parse operation data 60 opData, err := op.GetOperationData() 61 if err != nil { 62 return nil, fmt.Errorf("failed to parse operation: %w", err) 63 } 64 65 if opData == nil { 66 continue 67 } 68 69 // Check operation type 70 opType, _ := opData["type"].(string) 71 72 // Handle tombstone (deactivated DID) 73 if opType == "plc_tombstone" { 74 return nil, fmt.Errorf("DID has been deactivated") 75 } 76 77 // Initialize state on first operation 78 if state == nil { 79 state = &DIDState{ 80 DID: did, 81 RotationKeys: []string{}, 82 VerificationMethods: make(map[string]string), 83 AlsoKnownAs: []string{}, 84 Services: make(map[string]ServiceDefinition), 85 } 86 } 87 88 // Apply operation to state 89 applyOperationToState(state, opData) 90 } 91 92 if state == nil { 93 return nil, fmt.Errorf("no valid operations found") 94 } 95 96 return state, nil 97} 98 99// applyOperationToState updates state with data from an operation 100func applyOperationToState(state *DIDState, opData map[string]interface{}) { 101 // Update rotation keys 102 if rotKeys, ok := opData["rotationKeys"].([]interface{}); ok { 103 state.RotationKeys = make([]string, 0, len(rotKeys)) 104 for _, k := range rotKeys { 105 if keyStr, ok := k.(string); ok { 106 state.RotationKeys = append(state.RotationKeys, keyStr) 107 } 108 } 109 } 110 111 // Update verification methods 112 if vm, ok := opData["verificationMethods"].(map[string]interface{}); ok { 113 state.VerificationMethods = make(map[string]string) 114 for key, val := range vm { 115 if valStr, ok := val.(string); ok { 116 state.VerificationMethods[key] = valStr 117 } 118 } 119 } 120 121 // Handle legacy signingKey format 122 if signingKey, ok := opData["signingKey"].(string); ok { 123 if state.VerificationMethods == nil { 124 state.VerificationMethods = make(map[string]string) 125 } 126 state.VerificationMethods["atproto"] = signingKey 127 } 128 129 // Update alsoKnownAs 130 if aka, ok := opData["alsoKnownAs"].([]interface{}); ok { 131 state.AlsoKnownAs = make([]string, 0, len(aka)) 132 for _, a := range aka { 133 if akaStr, ok := a.(string); ok { 134 state.AlsoKnownAs = append(state.AlsoKnownAs, akaStr) 135 } 136 } 137 } 138 139 // Handle legacy handle format 140 if handle, ok := opData["handle"].(string); ok { 141 // Only set if alsoKnownAs is empty 142 if len(state.AlsoKnownAs) == 0 { 143 state.AlsoKnownAs = []string{"at://" + handle} 144 } 145 } 146 147 // Update services 148 if services, ok := opData["services"].(map[string]interface{}); ok { 149 state.Services = make(map[string]ServiceDefinition) 150 for key, svc := range services { 151 if svcMap, ok := svc.(map[string]interface{}); ok { 152 svcType, _ := svcMap["type"].(string) 153 endpoint, _ := svcMap["endpoint"].(string) 154 state.Services[key] = ServiceDefinition{ 155 Type: svcType, 156 Endpoint: normalizeServiceEndpoint(endpoint), 157 } 158 } 159 } 160 } 161 162 // Handle legacy service format 163 if service, ok := opData["service"].(string); ok { 164 if state.Services == nil { 165 state.Services = make(map[string]ServiceDefinition) 166 } 167 state.Services["atproto_pds"] = ServiceDefinition{ 168 Type: "AtprotoPersonalDataServer", 169 Endpoint: normalizeServiceEndpoint(service), 170 } 171 } 172} 173 174// StateToDIDDocument converts internal PLC state to W3C DID document format 175func StateToDIDDocument(state *DIDState) *DIDDocument { 176 // Base contexts - ALWAYS include multikey (matches PLC directory behavior) 177 contexts := []string{ 178 "https://www.w3.org/ns/did/v1", 179 "https://w3id.org/security/multikey/v1", 180 } 181 182 hasSecp256k1 := false 183 hasP256 := false 184 185 // Check verification method key types for additional contexts 186 for _, didKey := range state.VerificationMethods { 187 keyType := detectKeyType(didKey) 188 switch keyType { 189 case "secp256k1": 190 hasSecp256k1 = true 191 case "p256": 192 hasP256 = true 193 } 194 } 195 196 // Add suite-specific contexts only if those key types are present 197 if hasSecp256k1 { 198 contexts = append(contexts, "https://w3id.org/security/suites/secp256k1-2019/v1") 199 } 200 if hasP256 { 201 contexts = append(contexts, "https://w3id.org/security/suites/ecdsa-2019/v1") 202 } 203 204 doc := &DIDDocument{ 205 Context: contexts, 206 ID: state.DID, 207 AlsoKnownAs: []string{}, 208 VerificationMethod: []VerificationMethod{}, 209 Service: []Service{}, 210 } 211 212 // Copy alsoKnownAs if present 213 if len(state.AlsoKnownAs) > 0 { 214 doc.AlsoKnownAs = state.AlsoKnownAs 215 } 216 217 // Convert services 218 for id, svc := range state.Services { 219 doc.Service = append(doc.Service, Service{ 220 ID: "#" + id, 221 Type: svc.Type, 222 ServiceEndpoint: svc.Endpoint, 223 }) 224 } 225 226 // Keep verification methods with full DID 227 for id, didKey := range state.VerificationMethods { 228 doc.VerificationMethod = append(doc.VerificationMethod, VerificationMethod{ 229 ID: state.DID + "#" + id, 230 Type: "Multikey", 231 Controller: state.DID, 232 PublicKeyMultibase: ExtractMultibaseFromDIDKey(didKey), 233 }) 234 } 235 236 return doc 237} 238 239// detectKeyType detects the key type from did:key encoding 240func detectKeyType(didKey string) string { 241 multibase := ExtractMultibaseFromDIDKey(didKey) 242 243 if len(multibase) < 3 { 244 return "unknown" 245 } 246 247 // The 'z' is the base58btc multibase prefix 248 // Actual key starts at position 1 249 switch { 250 case multibase[1] == 'Q' && multibase[2] == '3': 251 return "secp256k1" // Starts with zQ3s 252 case multibase[1] == 'D' && multibase[2] == 'n': 253 return "p256" // Starts with zDn 254 case multibase[1] == '6' && multibase[2] == 'M': 255 return "ed25519" // Starts with z6Mk 256 default: 257 return "unknown" 258 } 259} 260 261// ExtractMultibaseFromDIDKey extracts the multibase string from did:key: format 262func ExtractMultibaseFromDIDKey(didKey string) string { 263 return strings.TrimPrefix(didKey, "did:key:") 264} 265 266// ValidateDIDFormat validates did:plc format 267func ValidateDIDFormat(did string) error { 268 if !strings.HasPrefix(did, "did:plc:") { 269 return fmt.Errorf("invalid DID method: must start with 'did:plc:'") 270 } 271 272 if len(did) != 32 { 273 return fmt.Errorf("invalid DID length: expected 32 chars, got %d", len(did)) 274 } 275 276 // Validate identifier part (24 chars, base32 alphabet) 277 identifier := strings.TrimPrefix(did, "did:plc:") 278 if len(identifier) != 24 { 279 return fmt.Errorf("invalid identifier length: expected 24 chars, got %d", len(identifier)) 280 } 281 282 // Check base32 alphabet (a-z, 2-7, no 0189) 283 for _, c := range identifier { 284 if !((c >= 'a' && c <= 'z') || (c >= '2' && c <= '7')) { 285 return fmt.Errorf("invalid character in identifier: %c (must be base32: a-z, 2-7)", c) 286 } 287 } 288 289 return nil 290} 291 292// FormatAuditLog formats operations as an audit log 293func FormatAuditLog(operations []PLCOperation) []AuditLogEntry { 294 log := make([]AuditLogEntry, 0, len(operations)) 295 296 for _, op := range operations { 297 // Parse operation for the log 298 opData, _ := op.GetOperationData() 299 300 entry := AuditLogEntry{ 301 DID: op.DID, 302 Operation: opData, 303 CID: op.CID, 304 Nullified: op.Nullified, 305 CreatedAt: op.CreatedAt.Format("2006-01-02T15:04:05.000Z"), 306 } 307 308 log = append(log, entry) 309 } 310 311 return log 312} 313 314func normalizeServiceEndpoint(endpoint string) string { 315 // If already has protocol, return as-is 316 if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { 317 return endpoint 318 } 319 320 // Legacy format: add https:// prefix 321 return "https://" + endpoint 322}