You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/locator/locator.go

732 lines
20 KiB

// Package locator implements the extension datekeys.capsule of a .dkk and
// what it points to (spec v0.11, §44.1): the data of the extension, which
// says what a key's capsule is and when it opens; the locator, an age file
// sealed with tlock for that date, which says where the capsule is; and the
// envelope, the .dkc encrypted with age and split into a header, which the
// locator carries, and a rest, the only thing that is kept outside, alone or
// inside another file.
//
// It does not download anything: a reader fetches the rest only when the
// person asks, after showing her the host or the CID (§44.1), and gives it
// to OpenEnvelope.
package locator
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"io"
"net/netip"
"strings"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/agewrap"
"g.activething.com/go/DateKeys/codec"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
)
// Limits of §44.1.
const (
// MaxAddresses is the most addresses of a locator.
MaxAddresses = 8
// MaxURILen is the longest address, in bytes.
MaxURILen = 1024
// MaxHeaderLen is the longest header of an envelope.
MaxHeaderLen = 1024
// Block is the unit of the plaintext of a locator: it measures exactly
// 4096 bytes, or the least multiple of 4096 that holds it.
Block = 4096
// maxSealed bounds a sealed locator that a reader decrypts.
maxSealed = 1 << 20
)
// Info is the data of the extension datekeys.capsule (spec §44.1).
type Info struct {
// Note is the copy of the public note of the capsule, "" for none.
Note string
// DateKey is the DateKey of the capsule: it says when it opens.
DateKey datekey.DateKey
// Sealed is the age file of the locator, sealed with tlock for the round
// of DateKey, nil for none.
Sealed []byte
}
// Extension returns the extension for the noncritical array of a .dkk.
func (i *Info) Extension() (extension.Extension, error) {
if d, err := datekey.Parse(i.DateKey.Compact()); err != nil || d != i.DateKey {
return extension.Extension{}, errors.New("locator: Info.DateKey is not a canonical DateKey")
}
if i.Sealed != nil && (len(i.Sealed) < 1 || len(i.Sealed) > maxSealed) {
return extension.Extension{}, fmt.Errorf("locator: a sealed locator of %d bytes, not 1 to %d", len(i.Sealed), maxSealed)
}
if i.Note != "" {
if err := extension.CheckNote(i.Note); err != nil {
return extension.Extension{}, err
}
}
var e codec.Encoder
pairs := 1
if i.Note != "" {
pairs++
}
if i.Sealed != nil {
pairs++
}
e.Map(pairs)
if i.Note != "" {
e.Uint(0)
e.Text(i.Note)
}
e.Uint(1)
e.Text(i.DateKey.Compact())
if i.Sealed != nil {
e.Uint(2)
e.Bstr(i.Sealed)
}
data, err := e.Out()
if err != nil {
return extension.Extension{}, err
}
return extension.New(extension.CapsuleID, 1, data)
}
// ParseInfo reads the data of a datekeys.capsule extension. A failure makes
// the extension unusable, not the .dkk (spec §54).
func ParseInfo(x extension.Extension) (*Info, error) {
if x.ID != extension.CapsuleID || x.Version != 1 || x.Data == nil {
return nil, fmt.Errorf("locator: not datekeys.capsule version 1 with data: %w", datekeys.ErrExtensionDataInvalid)
}
var i Info
var dk string
decode := func(d *codec.Decoder) error {
pairs, err := d.Map(3)
if err != nil {
return err
}
var seen uint
for range pairs {
k, err := d.Key()
if err != nil {
return err
}
switch k {
case 0:
i.Note, err = d.Text(extension.MaxNoteLen)
if err == nil {
err = extension.CheckNote(i.Note)
}
case 1:
dk, err = d.Text(1024)
case 2:
i.Sealed, err = d.Bstr(1, maxSealed)
default:
return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
}
if err != nil {
return fmt.Errorf("key %d: %w", k, err)
}
seen |= 1 << k
}
if seen&2 == 0 {
return fmt.Errorf("key 1 is missing: %w", datekeys.ErrNonCanonicalCBOR)
}
return d.EndMap()
}
encode := func(e *codec.Encoder) {
pairs := 1
if i.Note != "" {
pairs++
}
if i.Sealed != nil {
pairs++
}
e.Map(pairs)
if i.Note != "" {
e.Uint(0)
e.Text(i.Note)
}
e.Uint(1)
e.Text(dk)
if i.Sealed != nil {
e.Uint(2)
e.Bstr(i.Sealed)
}
}
if err := codec.Unmarshal(x.Data, decode, encode); err != nil {
return nil, fmt.Errorf("locator: datekeys.capsule: %w: %w", err, datekeys.ErrExtensionDataInvalid)
}
d, err := datekey.Parse(dk)
if err != nil || d.Compact() != dk {
return nil, fmt.Errorf("locator: compact_datekey is not a canonical DateKey: %w", datekeys.ErrExtensionDataInvalid)
}
i.DateKey = d
return &i, nil
}
// Address says where the rest of the envelope is.
type Address struct {
// URI is ASCII, RFC 3986, with the scheme https or ipfs (a CID v1), and
// no userinfo.
URI string
// Offset is the byte of the resource where the rest starts, 0 when the
// rest is the whole resource.
Offset uint64
}
// CheckURI checks an address with the rules of spec §44.1.
func CheckURI(uri string) error {
if uri == "" || len(uri) > MaxURILen {
return fmt.Errorf("locator: an address of %d bytes, not 1 to %d", len(uri), MaxURILen)
}
for i := 0; i < len(uri); i++ {
if uri[i] <= 0x20 || uri[i] >= 0x7f {
return errors.New("locator: an address with a character outside printable ASCII")
}
}
scheme, host, err := splitAuthority(uri)
if err != nil {
return err
}
switch scheme {
case "https":
return checkHost(host)
case "ipfs":
if !isCIDv1(host) {
return errors.New("locator: an ipfs address without a CID v1")
}
return nil
}
return fmt.Errorf("locator: the scheme %q: only https and ipfs", scheme)
}
// splitAuthority returns the scheme and the raw host of an address, without
// decoding anything: a percent sign in the authority, userinfo and a
// malformed port are refused, so that the host a reader shows is the host an
// HTTP client would use (spec v0.11, §44.1).
func splitAuthority(uri string) (scheme, host string, err error) {
scheme, rest, ok := strings.Cut(uri, "://")
if !ok || scheme == "" {
return "", "", errors.New("locator: an address without a scheme and ://")
}
authority := rest
if i := strings.IndexAny(rest, "/?#"); i >= 0 {
authority = rest[:i]
}
if strings.ContainsAny(authority, "%@\\") {
return "", "", errors.New("locator: an address with a percent sign, userinfo or a backslash in its authority")
}
host = authority
if scheme == "https" {
if strings.HasPrefix(authority, "[") {
end := strings.Index(authority, "]")
if end < 0 {
return "", "", errors.New("locator: an address with an unclosed IPv6 literal")
}
host = authority[:end+1]
if tail := authority[end+1:]; tail != "" {
if err := checkPort(tail); err != nil {
return "", "", err
}
}
} else if h, port, found := strings.Cut(authority, ":"); found {
host = h
if err := checkPort(":" + port); err != nil {
return "", "", err
}
}
}
return scheme, host, nil
}
func checkPort(s string) error {
if len(s) < 2 || s[0] != ':' || len(s) > 6 {
return errors.New("locator: an address with a malformed port")
}
n := 0
for _, c := range s[1:] {
if c < '0' || c > '9' {
return errors.New("locator: an address with a malformed port")
}
n = n*10 + int(c-'0')
}
if n < 1 || n > 65535 {
return errors.New("locator: an address with a port outside 1 to 65535")
}
return nil
}
// checkHost accepts a name of letters, digits, hyphens and dots, or an IP
// literal that is not loopback, private, link-local or unspecified: the spec
// forbids following a redirect to those, and an address that starts there
// would defeat the same rule (§44.1). A name whose last label is numeric, or
// is a hexadecimal number, is refused: some clients read it as an IPv4
// address in a form that netip does not.
func checkHost(host string) error {
if host == "" {
return errors.New("locator: an https address without a host")
}
if strings.HasPrefix(host, "[") {
a, err := netip.ParseAddr(strings.Trim(host, "[]"))
if err != nil || !publicIP(a) {
return errors.New("locator: an https address with an IPv6 literal that is not public")
}
return nil
}
labels := strings.Split(host, ".")
for _, l := range labels {
if l == "" || len(l) > 63 || l[0] == '-' || l[len(l)-1] == '-' {
return errors.New("locator: an https address with a malformed host")
}
for i := 0; i < len(l); i++ {
c := l[i]
if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-') {
return errors.New("locator: an https address whose host is not letters, digits and hyphens: write its punycode form")
}
}
}
last := labels[len(labels)-1]
if allDigits(last) || strings.HasPrefix(strings.ToLower(last), "0x") {
a, err := netip.ParseAddr(host)
if err != nil || !a.Is4() || !publicIP(a) {
return errors.New("locator: an https address with a numeric host that is not a public IPv4 address")
}
}
return nil
}
func allDigits(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return s != ""
}
func publicIP(a netip.Addr) bool {
return !(a.IsLoopback() || a.IsPrivate() || a.IsLinkLocalUnicast() || a.IsLinkLocalMulticast() || a.IsMulticast() || a.IsUnspecified())
}
// isCIDv1 reports whether s looks like a CID v1 in base32, which starts with
// 'b': it checks the alphabet and the length, not the multihash.
func isCIDv1(s string) bool {
if len(s) < 40 || len(s) > 128 || s[0] != 'b' {
return false
}
for i := 0; i < len(s); i++ {
if c := s[i]; !(c >= 'a' && c <= 'z' || c >= '2' && c <= '7') {
return false
}
}
return true
}
// Host returns what a reader shows before it downloads: the host of an https
// address, or the CID of an ipfs one (spec §44.1).
func (a Address) Host() string {
if CheckURI(a.URI) != nil {
return ""
}
_, host, _ := splitAuthority(a.URI)
return strings.Trim(host, "[]")
}
// Locator is the plaintext of the sealed locator (spec §44.1).
type Locator struct {
Addresses []Address
// EnvelopeKey is I_SOBRE, the raw X25519 identity of the envelope. SECRET.
EnvelopeKey [32]byte
// EnvelopeHeader is the age header of the envelope, MAC line included.
EnvelopeHeader []byte
// RestDigest is the SHA-256 of the rest, and RestSize its length.
RestDigest [32]byte
RestSize uint64
// CapsuleDigest is the SHA-256 of the .dkc (spec §43).
CapsuleDigest [32]byte
}
func (l *Locator) validate() error {
if len(l.Addresses) < 1 || len(l.Addresses) > MaxAddresses {
return fmt.Errorf("locator: %d addresses, not 1 to %d", len(l.Addresses), MaxAddresses)
}
for _, a := range l.Addresses {
if err := CheckURI(a.URI); err != nil {
return err
}
}
if n := len(l.EnvelopeHeader); n < 1 || n > MaxHeaderLen {
return fmt.Errorf("locator: an envelope header of %d bytes, not 1 to %d", n, MaxHeaderLen)
}
if l.RestSize > codec.MaxSafeUint {
return errors.New("locator: a rest larger than 2^53 - 1 bytes")
}
for _, a := range l.Addresses {
if a.Offset > codec.MaxSafeUint {
return errors.New("locator: an offset larger than 2^53 - 1")
}
}
return nil
}
// encode writes the map; pad < 0 leaves key 6 out.
func (l *Locator) encode(e *codec.Encoder, pad int) {
n := 6
if pad >= 0 {
n = 7
}
e.Map(n)
e.Uint(0)
e.Array(len(l.Addresses))
for _, a := range l.Addresses {
if a.Offset == 0 {
e.Map(1)
} else {
e.Map(2)
}
e.Uint(0)
e.Text(a.URI)
if a.Offset != 0 {
e.Uint(1)
e.Uint(a.Offset)
}
}
e.Uint(1)
e.Bstr(l.EnvelopeKey[:])
e.Uint(2)
e.Bstr(l.EnvelopeHeader)
e.Uint(3)
e.Bstr(l.RestDigest[:])
e.Uint(4)
e.Uint(l.RestSize)
e.Uint(5)
e.Bstr(l.CapsuleDigest[:])
if pad >= 0 {
e.Uint(6)
e.Bstr(make([]byte, pad))
}
}
func bstrHeadLen(n int) int {
switch {
case n < 24:
return 1
case n < 256:
return 2
case n < 65536:
return 3
}
return 5
}
// padFor returns the length of key 6 that makes the plaintext measure the
// least multiple of Block that holds it, or -1 when n0, the length without
// key 6, already is one. When no length of key 6 gives a given multiple, as
// happens at the boundaries of the CBOR length, it takes the next one.
func padFor(n0 int) int {
if n0%Block == 0 {
return -1
}
for total := (n0/Block + 1) * Block; ; total += Block {
for pad := 1; pad <= total-n0; pad++ {
if n0+1+bstrHeadLen(pad)+pad == total {
return pad
}
}
}
}
// PlaintextLength returns the length of the plaintext of a locator whose CBOR
// without key 6 measures base bytes: base when it already is a multiple of
// Block, and otherwise the least multiple that key 6 can fill exactly.
func PlaintextLength(base int) int {
pad := padFor(base)
if pad < 0 {
return base
}
return base + 1 + bstrHeadLen(pad) + pad
}
// Marshal returns the plaintext of the locator: CBOR with the profile of
// spec §58, completed with zeros in key 6 up to the least multiple of 4096
// bytes that holds it, so that its length does not tell how many addresses
// there are.
func (l *Locator) Marshal() ([]byte, error) {
if err := l.validate(); err != nil {
return nil, err
}
var e codec.Encoder
l.encode(&e, -1)
base, err := e.Out()
if err != nil {
return nil, err
}
pad := padFor(len(base))
if pad < 0 {
return base, nil
}
defer clear(base) // it holds I_SOBRE
var p codec.Encoder
l.encode(&p, pad)
out, err := p.Out()
if err != nil {
return nil, err
}
if len(out)%Block != 0 {
return nil, fmt.Errorf("locator: internal error: %d bytes of plaintext", len(out))
}
return out, nil
}
// Unmarshal reads the plaintext of a locator, checking its profile, its
// addresses and the length that Marshal gives.
func Unmarshal(b []byte) (*Locator, error) {
var l Locator
pad := -1
decode := func(d *codec.Decoder) error {
pairs, err := d.Map(7)
if err != nil {
return err
}
var seen uint
for range pairs {
k, err := d.Key()
if err != nil {
return err
}
switch k {
case 0:
err = decodeAddresses(d, &l)
case 1:
err = copyBstr(d, l.EnvelopeKey[:])
case 2:
l.EnvelopeHeader, err = d.Bstr(1, MaxHeaderLen)
case 3:
err = copyBstr(d, l.RestDigest[:])
case 4:
l.RestSize, err = d.Uint(codec.MaxSafeUint)
case 5:
err = copyBstr(d, l.CapsuleDigest[:])
case 6:
var z []byte
if z, err = d.Bstr(1, 1<<20); err == nil {
if len(bytes.Trim(z, "\x00")) != 0 {
return errors.New("the padding is not zeros")
}
pad = len(z)
}
default:
return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
}
if err != nil {
return fmt.Errorf("key %d: %w", k, err)
}
seen |= 1 << k
}
if seen&0x3f != 0x3f {
return fmt.Errorf("a key from 0 to 5 is missing: %w", datekeys.ErrNonCanonicalCBOR)
}
return d.EndMap()
}
encode := func(e *codec.Encoder) { l.encode(e, pad) }
if err := codec.Unmarshal(b, decode, encode); err != nil {
return nil, fmt.Errorf("locator: %w", err)
}
if err := l.validate(); err != nil {
return nil, err
}
// The length is the one Marshal gives: nothing else is canonical.
want, err := l.Marshal()
defer clear(want)
if err != nil || !bytes.Equal(want, b) {
return nil, fmt.Errorf("locator: the plaintext is not %d or the least multiple of %d that holds it: %w", Block, Block, datekeys.ErrNonCanonicalCBOR)
}
return &l, nil
}
func copyBstr(d *codec.Decoder, dst []byte) error {
b, err := d.Bstr(len(dst), len(dst))
if err != nil {
return err
}
copy(dst, b)
return nil
}
func decodeAddresses(d *codec.Decoder, l *Locator) error {
n, err := d.Array(MaxAddresses)
if err != nil {
return err
}
for range n {
pairs, err := d.Map(2)
if err != nil {
return err
}
var a Address
var seen uint
for range pairs {
k, err := d.Key()
if err != nil {
return err
}
switch k {
case 0:
a.URI, err = d.Text(MaxURILen)
case 1:
if a.Offset, err = d.Uint(codec.MaxSafeUint); err == nil && a.Offset == 0 {
err = fmt.Errorf("an offset of 0 is written by leaving it out: %w", datekeys.ErrNonCanonicalCBOR)
}
default:
return fmt.Errorf("address key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
}
if err != nil {
return err
}
seen |= 1 << k
}
if seen&1 == 0 {
return fmt.Errorf("an address without URI: %w", datekeys.ErrNonCanonicalCBOR)
}
if err := d.EndMap(); err != nil {
return err
}
l.Addresses = append(l.Addresses, a)
}
return nil
}
// Seal returns the locator as an age file with a single tlock stanza for the
// round of the DateKey (spec §44.1): nobody reads it before the date, the
// holder of the key included.
func Seal(p *profile.Profile, round uint64, l *Locator) ([]byte, error) {
plain, err := l.Marshal()
if err != nil {
return nil, err
}
defer clear(plain)
r, err := agewrap.NewTimeRecipient(p, round)
if err != nil {
return nil, err
}
var buf bytes.Buffer
w, err := age.Encrypt(&buf, r)
if err != nil {
return nil, err
}
if _, err := w.Write(plain); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Open opens a sealed locator with the release of its round, and reads its
// plaintext. A locator for another round or another chain does not open: it
// is unusable (spec §44.1).
func Open(p *profile.Profile, round uint64, release provider.Release, sealed []byte) (*Locator, error) {
id, err := agewrap.NewTimeIdentity(p, round, release)
if err != nil {
return nil, err
}
r, err := age.Decrypt(bytes.NewReader(sealed), id)
if err != nil {
return nil, fmt.Errorf("locator: %w", err)
}
plain, err := io.ReadAll(io.LimitReader(r, maxSealed))
if err != nil {
return nil, fmt.Errorf("locator: %w", err)
}
defer clear(plain)
return Unmarshal(plain)
}
// NewEnvelope encrypts the .dkc dkc with age for a new identity, I_SOBRE, and
// splits the age file: the locator it returns has the key, the header, the
// digests and the size of the rest, and no address yet; rest, with no mark,
// is what the person keeps outside. A caller adds the addresses where it
// stored rest, alone or inside another file, and then seals the locator.
func NewEnvelope(dkc []byte) (loc *Locator, rest []byte, err error) {
id, err := age.GenerateX25519Identity()
if err != nil {
return nil, nil, err
}
raw, err := agewrap.RawX25519Identity(id)
if err != nil {
return nil, nil, err
}
defer clear(raw)
var buf bytes.Buffer
w, err := age.Encrypt(&buf, id.Recipient())
if err != nil {
return nil, nil, err
}
if _, err := w.Write(dkc); err != nil {
return nil, nil, err
}
if err := w.Close(); err != nil {
return nil, nil, err
}
file := buf.Bytes()
end, err := headerEnd(file)
if err != nil {
return nil, nil, err
}
loc = &Locator{EnvelopeHeader: bytes.Clone(file[:end]), RestDigest: sha256.Sum256(file[end:]), RestSize: uint64(len(file) - end), CapsuleDigest: sha256.Sum256(dkc)}
copy(loc.EnvelopeKey[:], raw)
return loc, bytes.Clone(file[end:]), nil
}
// headerEnd returns the length of the age header of file, up to and
// including the line feed after the MAC line: the line that starts with
// "--- ". No line of the header before it starts so, and the lines of the
// body of a stanza are base64, which has no '-'.
func headerEnd(file []byte) (int, error) {
i := bytes.Index(file, []byte("\n--- "))
if i < 0 {
return 0, errors.New("locator: the age file has no MAC line")
}
j := bytes.IndexByte(file[i+1:], '\n')
if j < 0 {
return 0, errors.New("locator: the MAC line of the age file does not end")
}
return i + 1 + j + 1, nil
}
// OpenEnvelope joins the header of the locator and rest, which a reader got
// from an address, and decrypts the .dkc. It checks the size and the SHA-256
// of rest, and the SHA-256 of the .dkc, before the caller uses it (spec
// §44.1): they protect against whoever stores the rest, not against whoever
// wrote the .dkk.
func (l *Locator) OpenEnvelope(rest []byte) ([]byte, error) {
if uint64(len(rest)) != l.RestSize {
return nil, fmt.Errorf("locator: the rest is %d bytes, not %d", len(rest), l.RestSize)
}
if sha256.Sum256(rest) != l.RestDigest {
return nil, errors.New("locator: the SHA-256 of the rest is not the one of the locator")
}
id, err := agewrap.X25519IdentityFromRaw(l.EnvelopeKey[:])
if err != nil {
return nil, err
}
r, err := age.Decrypt(io.MultiReader(bytes.NewReader(l.EnvelopeHeader), bytes.NewReader(rest)), id)
if err != nil {
return nil, fmt.Errorf("locator: the envelope: %w", err)
}
dkc, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("locator: the envelope: %w", err)
}
if sha256.Sum256(dkc) != l.CapsuleDigest {
return nil, errors.New("locator: the SHA-256 of the .dkc is not the capsule_digest of the locator")
}
return dkc, nil
}

Powered by TurnKey Linux.