| 1 | package auth |
| 2 | |
| 3 | import ( |
| 4 | "encoding/base64" |
| 5 | "encoding/hex" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/gosuda/portal-tunnel/v2/portal/identity" |
| 11 | "github.com/gosuda/portal-tunnel/v2/types" |
| 12 | ) |
| 13 | |
| 14 | // SignRelayDescriptor returns a copy of desc with its Signature field |
| 15 | // populated by signing the canonical bytes with authority. The signature is |
| 16 | // recoverable, so verifiers do not need to know the public key out of band; |
| 17 | // they recover it from the signature and check it derives the descriptor's |
| 18 | // Address field. |
| 19 | func SignRelayDescriptor(desc types.RelayDescriptor, authority identity.Authority) (types.RelayDescriptor, error) { |
| 20 | if authority == nil { |
| 21 | return types.RelayDescriptor{}, errors.New("relay descriptor signing authority is required") |
| 22 | } |
| 23 | signingIdentity := authority.Identity() |
| 24 | if desc.Address == "" { |
| 25 | desc.Address = signingIdentity.Address |
| 26 | } |
| 27 | |
| 28 | desc.Signature = "" |
| 29 | normalized, err := identity.NormalizeRelayDescriptor(desc) |
| 30 | if err != nil { |
| 31 | return types.RelayDescriptor{}, fmt.Errorf("normalize relay descriptor for signing: %w", err) |
| 32 | } |
| 33 | if signingIdentity.Address != "" && !strings.EqualFold(strings.TrimSpace(signingIdentity.Address), strings.TrimSpace(normalized.Address)) { |
| 34 | return types.RelayDescriptor{}, errors.New("relay descriptor address does not match signing authority") |
| 35 | } |
| 36 | desc = normalized |
| 37 | |
| 38 | canonical, err := types.CanonicalBytes(desc) |
| 39 | if err != nil { |
| 40 | return types.RelayDescriptor{}, fmt.Errorf("canonicalize relay descriptor: %w", err) |
| 41 | } |
| 42 | signature, err := authority.SignSHA256Secp256k1(canonical) |
| 43 | if err != nil { |
| 44 | return types.RelayDescriptor{}, err |
| 45 | } |
| 46 | compactSignature, err := signature.Compact() |
| 47 | if err != nil { |
| 48 | return types.RelayDescriptor{}, err |
| 49 | } |
| 50 | |
| 51 | desc.Signature = base64.StdEncoding.EncodeToString(compactSignature) |
| 52 | return desc, nil |
| 53 | } |
| 54 | |
| 55 | // VerifyRelayDescriptor checks the descriptor's signature against its |
| 56 | // canonical bytes and confirms that the recovered signing key corresponds to |
| 57 | // the descriptor's Address field. It returns the verified normalized |
| 58 | // descriptor on success. |
| 59 | func VerifyRelayDescriptor(desc types.RelayDescriptor) (types.RelayDescriptor, error) { |
| 60 | rawSignature := strings.TrimSpace(desc.Signature) |
| 61 | if rawSignature == "" { |
| 62 | return types.RelayDescriptor{}, errors.New("relay descriptor is not signed") |
| 63 | } |
| 64 | |
| 65 | signature, err := base64.StdEncoding.DecodeString(rawSignature) |
| 66 | if err != nil { |
| 67 | return types.RelayDescriptor{}, fmt.Errorf("relay descriptor signature is invalid: base64 decode: %w", err) |
| 68 | } |
| 69 | |
| 70 | unsignedCopy := desc |
| 71 | unsignedCopy.Signature = "" |
| 72 | normalized, err := identity.NormalizeRelayDescriptor(unsignedCopy) |
| 73 | if err != nil { |
| 74 | return types.RelayDescriptor{}, fmt.Errorf("relay descriptor signature is invalid: normalize: %w", err) |
| 75 | } |
| 76 | canonical, err := types.CanonicalBytes(normalized) |
| 77 | if err != nil { |
| 78 | return types.RelayDescriptor{}, fmt.Errorf("canonicalize relay descriptor: %w", err) |
| 79 | } |
| 80 | |
| 81 | publicKey, err := identity.RecoverSHA256Secp256k1Compact(canonical, signature) |
| 82 | if err != nil { |
| 83 | return types.RelayDescriptor{}, fmt.Errorf("relay descriptor signature is invalid: %w", err) |
| 84 | } |
| 85 | |
| 86 | publicKeyHex := hex.EncodeToString(publicKey.SerializeCompressed()) |
| 87 | derivedAddress, err := identity.AddressFromCompressedPublicKeyHex(publicKeyHex) |
| 88 | if err != nil { |
| 89 | return types.RelayDescriptor{}, fmt.Errorf("derive address from recovered key: %w", err) |
| 90 | } |
| 91 | if !strings.EqualFold(strings.TrimSpace(derivedAddress), strings.TrimSpace(normalized.Address)) { |
| 92 | return types.RelayDescriptor{}, errors.New("relay descriptor address does not match recovered signing key") |
| 93 | } |
| 94 | normalized.Signature = rawSignature |
| 95 | return normalized, nil |
| 96 | } |