Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reusable ipns verify #292

Merged
merged 2 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions gateway/handler_ipns_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import (
"time"

"github.com/cespare/xxhash/v2"
"github.com/gogo/protobuf/proto"
ipath "github.com/ipfs/boxo/coreiface/path"
ipns_pb "github.com/ipfs/boxo/ipns/pb"
"github.com/ipfs/boxo/ipns"
"github.com/ipfs/go-cid"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
Expand Down Expand Up @@ -50,8 +49,7 @@ func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r
return false
}

var record ipns_pb.IpnsEntry
err = proto.Unmarshal(rawRecord, &record)
record, err := ipns.UnmarshalIpnsEntry(rawRecord)
if err != nil {
webError(w, err, http.StatusInternalServerError)
return false
Expand Down
141 changes: 141 additions & 0 deletions ipns/entries.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package ipns

import (
"bytes"
"encoding/json"
"fmt"
"time"

"github.com/gogo/protobuf/proto"
ipns_pb "github.com/ipfs/boxo/ipns/pb"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
ic "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
mbase "github.com/multiformats/go-multibase"
)

// IpnsInspectEntry contains the deserialized values from an IPNS Entry:
// https://github.com/ipfs/specs/blob/main/ipns/IPNS.md#record-serialization-format
type IpnsInspectEntry struct {
Value string
ValidityType *ipns_pb.IpnsEntry_ValidityType
Validity *time.Time
Sequence uint64
TTL *uint64
PublicKey string
SignatureV1 string
SignatureV2 string
Data interface{}
}

type IpnsInspectValidation struct {
Valid bool
Reason string
PublicKey peer.ID
}

func UnmarshalIpnsEntry(data []byte) (*ipns_pb.IpnsEntry, error) {
var entry ipns_pb.IpnsEntry
err := proto.Unmarshal(data, &entry)
if err != nil {
return nil, err
}

return &entry, nil
}

func InspectIpnsRecord(entry *ipns_pb.IpnsEntry) (*IpnsInspectEntry, error) {
encoder, err := mbase.EncoderByName("base64")
if err != nil {
return nil, err
}

result := IpnsInspectEntry{
Value: string(entry.Value),
ValidityType: entry.ValidityType,
Sequence: *entry.Sequence,
TTL: entry.Ttl,
PublicKey: encoder.Encode(entry.PubKey),
SignatureV1: encoder.Encode(entry.SignatureV1),
SignatureV2: encoder.Encode(entry.SignatureV2),
Data: nil,
}

if len(entry.Data) != 0 {
// This is hacky. The variable node (datamodel.Node) doesn't directly marshal
// to JSON. Therefore, we need to first decode from DAG-CBOR, then encode in
// DAG-JSON and finally unmarshal it from JSON. Since DAG-JSON is a subset
// of JSON, that should work. Then, we can store the final value in the
// result.Entry.Data for further inspection.
node, err := ipld.Decode(entry.Data, dagcbor.Decode)
if err != nil {
return nil, err
}

var buf bytes.Buffer
err = dagjson.Encode(node, &buf)
if err != nil {
return nil, err
}

err = json.Unmarshal(buf.Bytes(), &result.Data)
if err != nil {
return nil, err
}
}

validity, err := GetEOL(entry)
if err == nil {
result.Validity = &validity
}

return &result, nil
}

func Verify(key string, entry *ipns_pb.IpnsEntry) (*IpnsInspectValidation, error) {
id, err := peer.Decode(key)
if err != nil {
return nil, err
}

validation := &IpnsInspectValidation{
PublicKey: id,
}

pub, err := id.ExtractPublicKey()
if err != nil {
// Make sure it works with all those RSA that cannot be embedded into the
// Peer ID.
if len(entry.PubKey) > 0 {
pub, err = ic.UnmarshalPublicKey(entry.PubKey)
if err != nil {
return nil, err
}

// Verify the public key matches the name we are verifying.
entryID, err := peer.IDFromPublicKey(pub)

if err != nil {
return nil, err
}

if id != entryID {
return nil, fmt.Errorf("record public key does not match the verified name")
}
}
}
if err != nil {
return nil, err
}

err = Validate(pub, entry)
if err == nil {
validation.Valid = true
} else {
validation.Reason = err.Error()
}

return validation, nil
}