From 704ed25768ff954f448526eab1d0e73cfd1f21b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 7 Jan 2025 18:16:27 +0100 Subject: [PATCH 1/5] container: add a specification --- pkg/container/SPEC.md | 103 ++++++++++++++++++++++++++++++++++++++++ pkg/container/reader.go | 4 +- pkg/container/writer.go | 8 ++-- 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 pkg/container/SPEC.md diff --git a/pkg/container/SPEC.md b/pkg/container/SPEC.md new file mode 100644 index 0000000..be15c35 --- /dev/null +++ b/pkg/container/SPEC.md @@ -0,0 +1,103 @@ +# UCAN container Specification v0.1.0 + +## Editors + +* [Michael Muré], [Consensys] + +## Authors + +* [Michael Muré], [Consensys] +* [Hugo Dias] + +## Language + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [BCP 14] when, and only when, they appear in all capitals, as shown here. + +# 0 Abstract + +[User-Controlled Authorization Network (UCAN)][UCAN] is a trustless, secure, local-first, user-originated authorization and revocation scheme. This document describes a container format for transmitting one or more UCAN tokens as bytes, regardless of the transport. + +# 1 Introduction + +The UCAN spec itself is transport agnostic. This specification describes how to transfer one or more [UCAN] tokens bundled together, regardless of the transport. + +# 2 Container format + +## 2.1 Inner structure + +UCAN tokens, regardless of their kind ([Delegation], [Invocation], [Revocation], [Promise]) MUST be first signed and serialized into bytes according to their respective specification. As the token's CID is not part of the serialized container, any CID returned by this operation is to be ignored. + +All the tokens' bytes MUST be assembled in a [CBOR] array, which is then inserted as the value under the `ctn-v1` string key, in a CBOR map. The ordering of tokens in the array MUST NOT matter. For clarity, the CBOR shape is given below: + +```json +{ + "ctn-v1": [ + , + , + , + ] +} +``` + +## 2.2 Serialisation + +To serialize the container into bytes, the inner CBOR structure MUST be serialized into bytes according to the CBOR specification. The resulting bytes MAY be compressed by a supported algorithm, then MAY be encoded with a supported base encoding. + +The following compression algorithm are REQUIRED to be supported: +- [GZIP] + +The following base encoding combination are REQUIRED to be supported: +- base64, standard alphabet, padding +- base64, URL alphabet, no padding + +The CBOR bytes MUST be prepended by a single byte header to indicate the selection combination of base encoding and compression. This header value MUST be set according to the following table: + +| Header as hex | Header as ASCII | Base encoding | Compression | +|---------------|-----------------|--------------------------|----------------| +| 0x40 | @ | raw bytes | no compression | +| 0x42 | B | base64 std padding | no compression | +| 0x43 | C | base64 url | no compression | +| 0x4D | M | raw bytes | gzip | +| 0x4F | O | base64 std padding | gzip | +| 0x50 | P | base64 url | gzip | + +# 3 FAQ + +## 3.1 Why not include the UCAN CIDs? + +Several attacks are possible if UCAN tokens aren't validated. If CIDs aren't validated, at least two attacks are possible: [privilege escalation] and [cache poisoning], as UCAN delegation proofs depends on a correct hash-linked structure. + +By not including the CID in the container, the recipient is forced to hash (and thus validate) the CIDs for each token. If presented with a claimed CID paired with the token bytes, implementers could ignore CID validation, breaking a core part of the proof chain security model. Hash functions are very fast on a couple kilobytes of data so the overhead is still very low. It also reduces significantly the size of the container. + +## 3.2 Why compress? Why not always compress? + +Compression is a relatively demanding operation. As such, using it is a tradeoff between size on the wire and CPU/memory usage, both when writing and reading a container. The transport itself can make compression worthwhile or note: for example, HTTP/2 and HTTP/3 headers are already compressed, but HTTP/1 headers are not. This being highly contextual, the choice is left to the final implementer. + +# 4 Implementation recommendations + +## 4.1 Dissociate reader and writer + +While it tempting to write a single implementation to read and write a container, it is RECOMMENDED to separate the implementation into a reader and a writer. The writer can simply accept arbitrary tokens as bytes, while the reader provide a read-only view with convenient access functions. + +# 5 Acknowledgments + +Many thanks to all the [Fission] team and in particular to [Brooklyn Zelenka] for creating and pushing [UCAN] and other critical pieces like [WNFS], and generally being awesome and supportive people. + + + +[BCP 14]: https://www.rfc-editor.org/info/bcp14 +[Brooklyn Zelenka]: https://github.com/expede +[CBOR]: https://www.rfc-editor.org/rfc/rfc8949.html +[Consensys]: https://consensys.io/ +[Delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld +[Fission]: https://fission.codes +[GZIP]: https://datatracker.ietf.org/doc/html/rfc1952 +[Hugo Dias]: https://github.com/hugomrdias +[Invocation]: https://github.com/ucan-wg/invocation +[Michael Muré]: https://github.com/MichaelMure/ +[Promise]: https://github.com/ucan-wg/promise/tree/v1-rc1 +[Revocation]: https://github.com/ucan-wg/revocation/tree/first-draft +[UCAN]: https://github.com/ucan-wg/spec +[WNFS]: https://github.com/wnfs-wg +[cache poisoning]: https://en.wikipedia.org/wiki/Cache_poisoning +[privilede escalation]: https://en.wikipedia.org/wiki/Privilege_escalation diff --git a/pkg/container/reader.go b/pkg/container/reader.go index b9e1e98..431acf7 100644 --- a/pkg/container/reader.go +++ b/pkg/container/reader.go @@ -11,7 +11,7 @@ import ( "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/codec/cbor" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ucan-wg/go-ucan/token" @@ -102,7 +102,7 @@ func FromCbor(data []byte) (Reader, error) { // FromCborReader is the same as FromCbor, but with an io.Reader. func FromCborReader(r io.Reader) (Reader, error) { - n, err := ipld.DecodeStreaming(r, dagcbor.Decode) + n, err := ipld.DecodeStreaming(r, cbor.Decode) if err != nil { return nil, err } diff --git a/pkg/container/writer.go b/pkg/container/writer.go index 75b6fcf..60a148b 100644 --- a/pkg/container/writer.go +++ b/pkg/container/writer.go @@ -7,7 +7,7 @@ import ( "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/codec/cbor" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/ipld/go-ipld-prime/node/basicnode" @@ -27,7 +27,7 @@ func (ctn Writer) AddSealed(cid cid.Cid, data []byte) { const currentContainerVersion = "ctn-v1" -// ToCbor encode the container into a DAG-CBOR binary format. +// ToCbor encode the container into a CBOR binary format. func (ctn Writer) ToCbor() ([]byte, error) { var buf bytes.Buffer err := ctn.ToCborWriter(&buf) @@ -49,10 +49,10 @@ func (ctn Writer) ToCborWriter(w io.Writer) error { if err != nil { return err } - return ipld.EncodeStreaming(w, node, dagcbor.Encode) + return ipld.EncodeStreaming(w, node, cbor.Encode) } -// ToCborBase64 encode the container into a base64 encoded DAG-CBOR binary format. +// ToCborBase64 encode the container into a base64 encoded CBOR binary format. func (ctn Writer) ToCborBase64() (string, error) { var buf bytes.Buffer err := ctn.ToCborBase64Writer(&buf) From 8f3f1c775e9c14a9dcc2a1331a0441243219e302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 8 Jan 2025 17:31:04 +0100 Subject: [PATCH 2/5] container: implement header based serialization, also add SPEC test vectors --- pkg/container/SPEC.md | 16 ++-- pkg/container/packaging.go | 118 ++++++++++++++++++++++++ pkg/container/reader.go | 173 ++++++++++++++--------------------- pkg/container/serial_test.go | 82 +++++++++++++---- pkg/container/writer.go | 141 +++++++++++++++------------- 5 files changed, 333 insertions(+), 197 deletions(-) create mode 100644 pkg/container/packaging.go diff --git a/pkg/container/SPEC.md b/pkg/container/SPEC.md index be15c35..6c8fe60 100644 --- a/pkg/container/SPEC.md +++ b/pkg/container/SPEC.md @@ -52,14 +52,14 @@ The following base encoding combination are REQUIRED to be supported: The CBOR bytes MUST be prepended by a single byte header to indicate the selection combination of base encoding and compression. This header value MUST be set according to the following table: -| Header as hex | Header as ASCII | Base encoding | Compression | -|---------------|-----------------|--------------------------|----------------| -| 0x40 | @ | raw bytes | no compression | -| 0x42 | B | base64 std padding | no compression | -| 0x43 | C | base64 url | no compression | -| 0x4D | M | raw bytes | gzip | -| 0x4F | O | base64 std padding | gzip | -| 0x50 | P | base64 url | gzip | +| Header as hex | Header as ASCII | Base encoding | Compression | +|---------------|-----------------|-------------------------|----------------| +| 0x40 | @ | raw bytes | no compression | +| 0x42 | B | base64 std padding | no compression | +| 0x43 | C | base64 url (no padding) | no compression | +| 0x4D | M | raw bytes | gzip | +| 0x4F | O | base64 std padding | gzip | +| 0x50 | P | base64 url (no padding) | gzip | # 3 FAQ diff --git a/pkg/container/packaging.go b/pkg/container/packaging.go new file mode 100644 index 0000000..f1a8854 --- /dev/null +++ b/pkg/container/packaging.go @@ -0,0 +1,118 @@ +package container + +import ( + "compress/gzip" + "encoding/base64" + "errors" + "fmt" + "io" +) + +const containerVersionTag = "ctn-v1" + +type header byte + +const ( + headerRawBytes = header(0x40) + headerBase64StdPadding = header(0x42) + headerBase64URL = header(0x43) + headerRawBytesGzip = header(0x4D) + headerBase64StdPaddingGzip = header(0x4F) + headerBase64URLGzip = header(0x50) +) + +func (h header) encoder(w io.Writer) *payloadWriter { + res := &payloadWriter{rawWriter: w, writer: w, header: h} + + switch h { + case headerBase64StdPadding, headerBase64StdPaddingGzip: + b64Writer := base64.NewEncoder(base64.StdEncoding, res.writer) + res.writer = b64Writer + res.closers = append([]io.Closer{b64Writer}, res.closers...) + case headerBase64URL, headerBase64URLGzip: + b64Writer := base64.NewEncoder(base64.RawURLEncoding, res.writer) + res.writer = b64Writer + res.closers = append([]io.Closer{b64Writer}, res.closers...) + } + + switch h { + case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip: + gzipWriter := gzip.NewWriter(res.writer) + res.writer = gzipWriter + res.closers = append([]io.Closer{gzipWriter}, res.closers...) + } + + return res +} + +func decodePayload(r io.Reader) (io.Reader, error) { + headerBuf := make([]byte, 1) + _, err := r.Read(headerBuf) + if err != nil { + return nil, err + } + h := header(headerBuf[0]) + + switch h { + case headerRawBytes, + headerBase64StdPadding, + headerBase64URL, + headerRawBytesGzip, + headerBase64StdPaddingGzip, + headerBase64URLGzip: + default: + return nil, fmt.Errorf("unknown container header") + } + + switch h { + case headerBase64StdPadding, headerBase64StdPaddingGzip: + r = base64.NewDecoder(base64.StdEncoding, r) + case headerBase64URL, headerBase64URLGzip: + r = base64.NewDecoder(base64.RawURLEncoding, r) + } + + switch h { + case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip: + gzipReader, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + r = gzipReader + } + + return r, nil +} + +var _ io.WriteCloser = &payloadWriter{} + +// payloadWriter is tasked with two things: +// - prepend the header byte +// - call Close() on all the underlying io.Writer +type payloadWriter struct { + rawWriter io.Writer + writer io.Writer + header header + headerWrote bool + closers []io.Closer +} + +func (w *payloadWriter) Write(p []byte) (n int, err error) { + if !w.headerWrote { + _, err := w.rawWriter.Write([]byte{byte(w.header)}) + if err != nil { + return 0, err + } + w.headerWrote = true + } + return w.writer.Write(p) +} + +func (w *payloadWriter) Close() error { + var errs error + for _, closer := range w.closers { + if err := closer.Close(); err != nil { + errs = errors.Join(errs, err) + } + } + return errs +} diff --git a/pkg/container/reader.go b/pkg/container/reader.go index 431acf7..37d71d4 100644 --- a/pkg/container/reader.go +++ b/pkg/container/reader.go @@ -2,7 +2,6 @@ package container import ( "bytes" - "encoding/base64" "errors" "fmt" "io" @@ -25,6 +24,72 @@ var ErrMultipleInvocations = fmt.Errorf("multiple invocations") // Reader is a token container reader. It exposes the tokens conveniently decoded. type Reader map[cid.Cid]token.Token +// FromBytes decodes a container from a []byte +func FromBytes(data []byte) (Reader, error) { + return FromReader(bytes.NewReader(data)) +} + +// FromString decodes a container from a string +func FromString(s string) (Reader, error) { + return FromReader(strings.NewReader(s)) +} + +// FromReader decodes a container from an io.Reader. +func FromReader(r io.Reader) (Reader, error) { + payload, err := decodePayload(r) + if err != nil { + return nil, err + } + + n, err := ipld.DecodeStreaming(payload, cbor.Decode) + if err != nil { + return nil, err + } + if n.Kind() != datamodel.Kind_Map { + return nil, fmt.Errorf("invalid container format: expected map") + } + if n.Length() != 1 { + return nil, fmt.Errorf("invalid container format: expected single version key") + } + + // get the first (and only) key-value pair + it := n.MapIterator() + key, tokensNode, err := it.Next() + if err != nil { + return nil, err + } + + version, err := key.AsString() + if err != nil { + return nil, fmt.Errorf("invalid container format: version must be string") + } + if version != containerVersionTag { + return nil, fmt.Errorf("unsupported container version: %s", version) + } + + if tokensNode.Kind() != datamodel.Kind_List { + return nil, fmt.Errorf("invalid container format: tokens must be a list") + } + + ctn := make(Reader, tokensNode.Length()) + it2 := tokensNode.ListIterator() + for !it2.Done() { + _, val, err := it2.Next() + if err != nil { + return nil, err + } + data, err := val.AsBytes() + if err != nil { + return nil, err + } + err = ctn.addToken(data) + if err != nil { + return nil, err + } + } + return ctn, nil +} + // GetToken returns an arbitrary decoded token, from its CID. // If not found, ErrNotFound is returned. func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) { @@ -65,7 +130,7 @@ func (ctn Reader) GetAllDelegations() iter.Seq2[cid.Cid, *delegation.Token] { // GetInvocation returns a single invocation.Token. // If none are found, ErrNotFound is returned. -// If more than one invocation exist, ErrMultipleInvocations is returned. +// If more than one invocation exists, ErrMultipleInvocations is returned. func (ctn Reader) GetInvocation() (*invocation.Token, error) { var res *invocation.Token for _, t := range ctn { @@ -95,110 +160,6 @@ func (ctn Reader) GetAllInvocations() iter.Seq2[cid.Cid, *invocation.Token] { } } -// FromCbor decodes a DAG-CBOR encoded container. -func FromCbor(data []byte) (Reader, error) { - return FromCborReader(bytes.NewReader(data)) -} - -// FromCborReader is the same as FromCbor, but with an io.Reader. -func FromCborReader(r io.Reader) (Reader, error) { - n, err := ipld.DecodeStreaming(r, cbor.Decode) - if err != nil { - return nil, err - } - if n.Kind() != datamodel.Kind_Map { - return nil, fmt.Errorf("invalid container format: expected map") - } - if n.Length() != 1 { - return nil, fmt.Errorf("invalid container format: expected single version key") - } - - // get the first (and only) key-value pair - it := n.MapIterator() - key, tokensNode, err := it.Next() - if err != nil { - return nil, err - } - - version, err := key.AsString() - if err != nil { - return nil, fmt.Errorf("invalid container format: version must be string") - } - if version != currentContainerVersion { - return nil, fmt.Errorf("unsupported container version: %s", version) - } - - if tokensNode.Kind() != datamodel.Kind_List { - return nil, fmt.Errorf("invalid container format: tokens must be a list") - } - - ctn := make(Reader, tokensNode.Length()) - it2 := tokensNode.ListIterator() - for !it2.Done() { - _, val, err := it2.Next() - if err != nil { - return nil, err - } - data, err := val.AsBytes() - if err != nil { - return nil, err - } - err = ctn.addToken(data) - if err != nil { - return nil, err - } - } - return ctn, nil -} - -// FromCborBase64 decodes a base64 DAG-CBOR encoded container. -func FromCborBase64(data string) (Reader, error) { - return FromCborBase64Reader(strings.NewReader(data)) -} - -// FromCborBase64Reader is the same as FromCborBase64, but with an io.Reader. -func FromCborBase64Reader(r io.Reader) (Reader, error) { - return FromCborReader(base64.NewDecoder(base64.StdEncoding, r)) -} - -// FromCar decodes a CAR file encoded container. -func FromCar(data []byte) (Reader, error) { - return FromCarReader(bytes.NewReader(data)) -} - -// FromCarReader is the same as FromCar, but with an io.Reader. -func FromCarReader(r io.Reader) (Reader, error) { - _, it, err := readCar(r) - if err != nil { - return nil, err - } - - ctn := make(Reader) - - for block, err := range it { - if err != nil { - return nil, err - } - - err = ctn.addToken(block.data) - if err != nil { - return nil, err - } - } - - return ctn, nil -} - -// FromCarBase64 decodes a base64 CAR file encoded container. -func FromCarBase64(data string) (Reader, error) { - return FromCarReader(strings.NewReader(data)) -} - -// FromCarBase64Reader is the same as FromCarBase64, but with an io.Reader. -func FromCarBase64Reader(r io.Reader) (Reader, error) { - return FromCarReader(base64.NewDecoder(base64.StdEncoding, r)) -} - func (ctn Reader) addToken(data []byte) error { tkn, c, err := token.FromSealed(data) if err != nil { diff --git a/pkg/container/serial_test.go b/pkg/container/serial_test.go index 3894e7a..37ed3f7 100644 --- a/pkg/container/serial_test.go +++ b/pkg/container/serial_test.go @@ -22,14 +22,22 @@ import ( func TestContainerRoundTrip(t *testing.T) { for _, tc := range []struct { - name string - writer func(ctn Writer, w io.Writer) error - reader func(io.Reader) (Reader, error) + name string + expectedHeader header + writer any }{ - {"car", Writer.ToCarWriter, FromCarReader}, - {"carBase64", Writer.ToCarBase64Writer, FromCarBase64Reader}, - {"cbor", Writer.ToCborWriter, FromCborReader}, - {"cborBase64", Writer.ToCborBase64Writer, FromCborBase64Reader}, + {"Bytes", headerRawBytes, Writer.ToBytes}, + {"BytesWriter", headerRawBytes, Writer.ToBytesWriter}, + {"BytesGzipped", headerRawBytesGzip, Writer.ToBytesGzipped}, + {"BytesGzippedWriter", headerRawBytesGzip, Writer.ToBytesGzippedWriter}, + {"Base64StdPadding", headerBase64StdPadding, Writer.ToBase64StdPadding}, + {"Base64StdPaddingWriter", headerBase64StdPadding, Writer.ToBase64StdPaddingWriter}, + {"Base64StdPaddingGzipped", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzipped}, + {"Base64StdPaddingGzippedWriter", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzippedWriter}, + {"Base64URL", headerBase64URL, Writer.ToBase64URL}, + {"Base64URLWriter", headerBase64URL, Writer.ToBase64URLWriter}, + {"Base64URLGzip", headerBase64URLGzip, Writer.ToBase64URLGzip}, + {"Base64URLGzipWriter", headerBase64URLGzip, Writer.ToBase64URLGzipWriter}, } { t.Run(tc.name, func(t *testing.T) { tokens := make(map[cid.Cid]*delegation.Token) @@ -44,16 +52,48 @@ func TestContainerRoundTrip(t *testing.T) { dataSize += len(data) } - buf := bytes.NewBuffer(nil) + var reader Reader + var serialLen int - err := tc.writer(writer, buf) - require.NoError(t, err) + switch fn := tc.writer.(type) { + case func(ctn Writer, w io.Writer) error: + buf := bytes.NewBuffer(nil) + err := fn(writer, buf) + require.NoError(t, err) + serialLen = buf.Len() - t.Logf("data size %d", dataSize) - t.Logf("container overhead: %d%%, %d bytes", int(float32(buf.Len()-dataSize)/float32(dataSize)*100.0), buf.Len()-dataSize) + h, err := buf.ReadByte() + require.NoError(t, err) + require.Equal(t, byte(tc.expectedHeader), h) + err = buf.UnreadByte() + require.NoError(t, err) - reader, err := tc.reader(bytes.NewReader(buf.Bytes())) - require.NoError(t, err) + reader, err = FromReader(bytes.NewReader(buf.Bytes())) + require.NoError(t, err) + + case func(ctn Writer) ([]byte, error): + b, err := fn(writer) + require.NoError(t, err) + serialLen = len(b) + + require.Equal(t, byte(tc.expectedHeader), b[0]) + + reader, err = FromBytes(b) + require.NoError(t, err) + + case func(ctn Writer) (string, error): + s, err := fn(writer) + require.NoError(t, err) + serialLen = len(s) + + require.Equal(t, byte(tc.expectedHeader), s[0]) + + reader, err = FromString(s) + require.NoError(t, err) + } + + t.Logf("data size %d, container size %d, overhead: %d%%, %d bytes", + dataSize, serialLen, int(float32(serialLen-dataSize)/float32(dataSize)*100.0), serialLen-dataSize) for c, dlg := range tokens { tknRead, err := reader.GetToken(c) @@ -98,10 +138,12 @@ func BenchmarkContainerSerialisation(b *testing.B) { writer func(ctn Writer, w io.Writer) error reader func(io.Reader) (Reader, error) }{ - {"car", Writer.ToCarWriter, FromCarReader}, - {"carBase64", Writer.ToCarBase64Writer, FromCarBase64Reader}, - {"cbor", Writer.ToCborWriter, FromCborReader}, - {"cborBase64", Writer.ToCborBase64Writer, FromCborBase64Reader}, + {"Bytes", Writer.ToBytesWriter, FromReader}, + {"BytesGzipped", Writer.ToBytesGzippedWriter, FromReader}, + {"Base64StdPadding", Writer.ToBase64StdPaddingWriter, FromReader}, + {"Base64StdPaddingGzipped", Writer.ToBase64StdPaddingGzippedWriter, FromReader}, + {"Base64URL", Writer.ToBase64URLWriter, FromReader}, + {"Base64URLGzip", Writer.ToBase64URLGzipWriter, FromReader}, } { writer := NewWriter() @@ -184,7 +226,7 @@ func FuzzContainerRead(f *testing.F) { _, c, data := randToken() writer.AddSealed(c, data) } - data, err := writer.ToCbor() + data, err := writer.ToBytes() require.NoError(f, err) f.Add(data) @@ -194,7 +236,7 @@ func FuzzContainerRead(f *testing.F) { start := time.Now() // search for panics - _, _ = FromCbor(data) + _, _ = FromBytes(data) if time.Since(start) > 100*time.Millisecond { panic("too long") diff --git a/pkg/container/writer.go b/pkg/container/writer.go index 60a148b..6f366b8 100644 --- a/pkg/container/writer.go +++ b/pkg/container/writer.go @@ -2,7 +2,6 @@ package container import ( "bytes" - "encoding/base64" "io" "github.com/ipfs/go-cid" @@ -25,22 +24,92 @@ func (ctn Writer) AddSealed(cid cid.Cid, data []byte) { ctn[cid] = data } -const currentContainerVersion = "ctn-v1" +// ToBytes encode the container into raw bytes. +func (ctn Writer) ToBytes() ([]byte, error) { + return ctn.toBytes(headerRawBytes) +} -// ToCbor encode the container into a CBOR binary format. -func (ctn Writer) ToCbor() ([]byte, error) { +// ToBytesWriter is the same as ToBytes, but with an io.Writer. +func (ctn Writer) ToBytesWriter(w io.Writer) error { + return ctn.toWriter(headerRawBytes, w) +} + +// ToBytesGzipped encode the container into gzipped bytes. +func (ctn Writer) ToBytesGzipped() ([]byte, error) { + return ctn.toBytes(headerRawBytesGzip) +} + +// ToBytesGzippedWriter is the same as ToBytesGzipped, but with an io.Writer. +func (ctn Writer) ToBytesGzippedWriter(w io.Writer) error { + return ctn.toWriter(headerRawBytesGzip, w) +} + +// ToBase64StdPadding encode the container into a base64 string, with standard encoding and padding. +func (ctn Writer) ToBase64StdPadding() (string, error) { + return ctn.toString(headerBase64StdPadding) +} + +// ToBase64StdPaddingWriter is the same as ToBase64StdPadding, but with an io.Writer. +func (ctn Writer) ToBase64StdPaddingWriter(w io.Writer) error { + return ctn.toWriter(headerBase64StdPadding, w) +} + +// ToBase64StdPaddingGzipped encode the container into a pre-gzipped base64 string, with standard encoding and padding. +func (ctn Writer) ToBase64StdPaddingGzipped() (string, error) { + return ctn.toString(headerBase64StdPaddingGzip) +} + +// ToBase64StdPaddingGzippedWriter is the same as ToBase64StdPaddingGzipped, but with an io.Writer. +func (ctn Writer) ToBase64StdPaddingGzippedWriter(w io.Writer) error { + return ctn.toWriter(headerBase64StdPaddingGzip, w) +} + +// ToBase64URL encode the container into base64 string, with URL-safe encoding and no padding. +func (ctn Writer) ToBase64URL() (string, error) { + return ctn.toString(headerBase64URL) +} + +// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer. +func (ctn Writer) ToBase64URLWriter(w io.Writer) error { + return ctn.toWriter(headerBase64URL, w) +} + +// ToBase64URL encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding. +func (ctn Writer) ToBase64URLGzip() (string, error) { + return ctn.toString(headerBase64URLGzip) +} + +// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer. +func (ctn Writer) ToBase64URLGzipWriter(w io.Writer) error { + return ctn.toWriter(headerBase64URLGzip, w) +} + +func (ctn Writer) toBytes(header header) ([]byte, error) { var buf bytes.Buffer - err := ctn.ToCborWriter(&buf) + err := ctn.toWriter(header, &buf) if err != nil { return nil, err } return buf.Bytes(), nil } -// ToCborWriter is the same as ToCbor, but with an io.Writer. -func (ctn Writer) ToCborWriter(w io.Writer) error { +func (ctn Writer) toString(header header) (string, error) { + var buf bytes.Buffer + err := ctn.toWriter(header, &buf) + if err != nil { + return "", err + } + return buf.String(), nil +} + +func (ctn Writer) toWriter(header header, w io.Writer) (err error) { + encoder := header.encoder(w) + + defer func() { + err = encoder.Close() + }() node, err := qp.BuildMap(basicnode.Prototype.Any, 1, func(ma datamodel.MapAssembler) { - qp.MapEntry(ma, currentContainerVersion, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) { + qp.MapEntry(ma, containerVersionTag, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) { for _, data := range ctn { qp.ListEntry(la, qp.Bytes(data)) } @@ -49,60 +118,6 @@ func (ctn Writer) ToCborWriter(w io.Writer) error { if err != nil { return err } - return ipld.EncodeStreaming(w, node, cbor.Encode) -} -// ToCborBase64 encode the container into a base64 encoded CBOR binary format. -func (ctn Writer) ToCborBase64() (string, error) { - var buf bytes.Buffer - err := ctn.ToCborBase64Writer(&buf) - if err != nil { - return "", err - } - return buf.String(), nil -} - -// ToCborBase64Writer is the same as ToCborBase64, but with an io.Writer. -func (ctn Writer) ToCborBase64Writer(w io.Writer) error { - w2 := base64.NewEncoder(base64.StdEncoding, w) - defer w2.Close() - return ctn.ToCborWriter(w2) -} - -// ToCar encode the container into a CAR file. -func (ctn Writer) ToCar() ([]byte, error) { - var buf bytes.Buffer - err := ctn.ToCarWriter(&buf) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -// ToCarWriter is the same as ToCar, but with an io.Writer. -func (ctn Writer) ToCarWriter(w io.Writer) error { - return writeCar(w, nil, func(yield func(carBlock, error) bool) { - for c, data := range ctn { - if !yield(carBlock{c: c, data: data}, nil) { - return - } - } - }) -} - -// ToCarBase64 encode the container into a base64 encoded CAR file. -func (ctn Writer) ToCarBase64() (string, error) { - var buf bytes.Buffer - err := ctn.ToCarBase64Writer(&buf) - if err != nil { - return "", err - } - return buf.String(), nil -} - -// ToCarBase64Writer is the same as ToCarBase64, but with an io.Writer. -func (ctn Writer) ToCarBase64Writer(w io.Writer) error { - w2 := base64.NewEncoder(base64.StdEncoding, w) - defer w2.Close() - return ctn.ToCarWriter(w2) + return ipld.EncodeStreaming(encoder, node, cbor.Encode) } From c792a4cce537979ca7e84466186047429009d479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 8 Jan 2025 18:55:08 +0100 Subject: [PATCH 3/5] container: typo and clarity in the spec --- pkg/container/SPEC.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/container/SPEC.md b/pkg/container/SPEC.md index 6c8fe60..9f92720 100644 --- a/pkg/container/SPEC.md +++ b/pkg/container/SPEC.md @@ -25,9 +25,11 @@ The UCAN spec itself is transport agnostic. This specification describes how to ## 2.1 Inner structure -UCAN tokens, regardless of their kind ([Delegation], [Invocation], [Revocation], [Promise]) MUST be first signed and serialized into bytes according to their respective specification. As the token's CID is not part of the serialized container, any CID returned by this operation is to be ignored. +UCAN tokens, regardless of their kind ([Delegation], [Invocation], [Revocation], [Promise]) MUST be first signed and serialized into DAG-CBOR bytes according to their respective specification. As the token's CID is not part of the serialized container, any CID returned by this operation is to be ignored. -All the tokens' bytes MUST be assembled in a [CBOR] array, which is then inserted as the value under the `ctn-v1` string key, in a CBOR map. The ordering of tokens in the array MUST NOT matter. For clarity, the CBOR shape is given below: +All the tokens' bytes MUST be assembled in a [CBOR] array, which is then inserted as the value under the `ctn-v1` string key, in a CBOR map. The ordering of tokens in the array MUST NOT matter. Also, this array SHOULD NOT have duplicate entries. + +For clarity, the CBOR shape is given below: ```json { @@ -41,12 +43,12 @@ All the tokens' bytes MUST be assembled in a [CBOR] array, which is then inserte ## 2.2 Serialisation -To serialize the container into bytes, the inner CBOR structure MUST be serialized into bytes according to the CBOR specification. The resulting bytes MAY be compressed by a supported algorithm, then MAY be encoded with a supported base encoding. +To serialize the container into bytes, the inner CBOR structure MUST then be serialized into bytes according to the CBOR specification. The resulting bytes MAY be compressed by a supported algorithm, then MAY be encoded with a supported base encoding. -The following compression algorithm are REQUIRED to be supported: +The following compression algorithms are REQUIRED to be supported: - [GZIP] -The following base encoding combination are REQUIRED to be supported: +The following base encoding combinations are REQUIRED to be supported: - base64, standard alphabet, padding - base64, URL alphabet, no padding @@ -61,23 +63,25 @@ The CBOR bytes MUST be prepended by a single byte header to indicate the selecti | 0x4F | O | base64 std padding | gzip | | 0x50 | P | base64 url (no padding) | gzip | +For clarity, the resulting serialisation is in the form of `
`. + # 3 FAQ ## 3.1 Why not include the UCAN CIDs? Several attacks are possible if UCAN tokens aren't validated. If CIDs aren't validated, at least two attacks are possible: [privilege escalation] and [cache poisoning], as UCAN delegation proofs depends on a correct hash-linked structure. -By not including the CID in the container, the recipient is forced to hash (and thus validate) the CIDs for each token. If presented with a claimed CID paired with the token bytes, implementers could ignore CID validation, breaking a core part of the proof chain security model. Hash functions are very fast on a couple kilobytes of data so the overhead is still very low. It also reduces significantly the size of the container. +By not including the CID in the container, the recipient is forced to hash (and thus validate) the CIDs for each token. If presented with a claimed CID paired with the token bytes, implementers could ignore CID validation, breaking a core part of the proof chain security model. Hash functions are very fast on a couple kilobytes of data so the overhead is still very low. It also significantly reduces the size of the container. ## 3.2 Why compress? Why not always compress? -Compression is a relatively demanding operation. As such, using it is a tradeoff between size on the wire and CPU/memory usage, both when writing and reading a container. The transport itself can make compression worthwhile or note: for example, HTTP/2 and HTTP/3 headers are already compressed, but HTTP/1 headers are not. This being highly contextual, the choice is left to the final implementer. +Compression is a relatively demanding operation. As such, using it is a tradeoff between size on the wire and CPU/memory usage, both when writing and reading a container. The transport itself can make compression worthwhile or not: for example, HTTP/2 and HTTP/3 headers are already compressed, but HTTP/1 headers are not. This being highly contextual, the choice is left to the final implementer. # 4 Implementation recommendations ## 4.1 Dissociate reader and writer -While it tempting to write a single implementation to read and write a container, it is RECOMMENDED to separate the implementation into a reader and a writer. The writer can simply accept arbitrary tokens as bytes, while the reader provide a read-only view with convenient access functions. +While it is tempting to write a single implementation to read and write a container, it is RECOMMENDED to separate the implementation into a reader and a writer. The writer can simply accept arbitrary tokens as bytes, while the reader provides a read-only view with convenient access functions. # 5 Acknowledgments From 00dbd975b1ed109715e7117039abf80e2f76ff98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 8 Jan 2025 18:59:30 +0100 Subject: [PATCH 4/5] container: add test vectors --- pkg/container/testvectors/Base64StdPadding | 1 + .../testvectors/Base64StdPaddingGzipped | 1 + pkg/container/testvectors/Base64URL | 1 + pkg/container/testvectors/Base64URLGzip | 1 + pkg/container/testvectors/Bytes | Bin 0 -> 4180 bytes pkg/container/testvectors/BytesGzipped | Bin 0 -> 2336 bytes 6 files changed, 4 insertions(+) create mode 100644 pkg/container/testvectors/Base64StdPadding create mode 100644 pkg/container/testvectors/Base64StdPaddingGzipped create mode 100644 pkg/container/testvectors/Base64URL create mode 100644 pkg/container/testvectors/Base64URLGzip create mode 100644 pkg/container/testvectors/Bytes create mode 100644 pkg/container/testvectors/BytesGzipped diff --git a/pkg/container/testvectors/Base64StdPadding b/pkg/container/testvectors/Base64StdPadding new file mode 100644 index 0000000..58936c0 --- /dev/null +++ b/pkg/container/testvectors/Base64StdPadding @@ -0,0 +1 @@ +BoWZjdG4tdjGKWQGeglhAa8vQAimsd6982rgPP0e1ccInV/4cjWzjR71iyXGc2qVOGyEEZMT+e/Zfaw1WzVS5Ybgtpjw5/845jAPNbJv1DqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdnF0eDhkWFZ3NzZkTnc2U0FZUXJ3cFEzQlI4S3NwZ2FUN0Rwd3F3OW1zbXhjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcDZmMng2TW1xY1BDVWJhRVBxM0VqYjc4cjI3TFVKTEJWYUxjZ2hZaFR6TXJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcDZmMng2TW1xY1BDVWJhRVBxM0VqYjc4cjI3TFVKTEJWYUxjZ2hZaFR6TXJkbWV0YaNoNTIxZDIzZDBqMzMwNTJkZjgzOGg3NDVlZWFlZGpkYzg2NDM2M2QxaDgzZjY5OGYzajNhMDA3ZWY4Mjllbm9uY2VMwB+HSucZ5j+7FG/wWQGeglhAFgW0vCDmL/dzrIIT5oVRXsA1b/Fk5AiKhFlAVXRSXFoKuCixvn17vePm8anPkPewtZS7QiekJYwt9a0Aoe/CBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcGNrS01FM3c3UWlvdWV3WkpLUFI0cDF5ak4yUDRjOXF3aDc1NVFqeTNyMURjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ramlzQmZ3RE5SSHlYZmljd1k1M2MxaVpHdnBqaWVlclBuc1JxSEN2emhvOXpjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ramlzQmZ3RE5SSHlYZmljd1k1M2MxaVpHdnBqaWVlclBuc1JxSEN2emhvOXpkbWV0YaNoMTllZGI5NWFqN2ZkNThkYjc5M2gyY2FhY2NiYmo0NWJkNmQ1ODY2aDg4ZmY0N2E3amU2YjgzNjZlNGRlbm9uY2VMCrOVvOHN5HLH2khEWQGeglhAf4mTe11QRPxtukpFKL6IaKRxrZPSytRgfOHYXGHLrImx0j0qbdPkQLaQeajQAG2vYnrjqG74v+lRhHIFTLsRCKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcWREc2FqdDhUQXBkeGtNbmRHQXoxa1FNY0w4bWtxYXNWR1ZoZm5MRnRIeFRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1raDlicHdRWjNycFZ0dlZXOFhtRUJEc01ucUJBZG00Q2MzczFWeTE3VUhDR1NjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raDlicHdRWjNycFZ0dlZXOFhtRUJEc01ucUJBZG00Q2MzczFWeTE3VUhDR1NkbWV0YaNoMDhjYTQ0MmNqMTBiNmU5Y2JjY2hjNTZmMjUwM2o1ZTJlNmU0ZDNiaGU3ODNhNTcxajY2OWVmZTI2Y2Nlbm9uY2VMm+hnFEzvWMyIwPWpWQGeglhAg2jLUqHQRlsIoBt7jaif9n1AFiuODDXx0lxtIhZLK52E9AjdPl+MRYaE4HrYXLUZdsACOovH6YrS8onV0E2ZA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rb1dIQXN2d2ZRWnZDamdCYWhlSjRMWVlXYWJ2MTVjcmpSVGJEWWpGM2ZHbWVjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcXFhcVdMZEVRWTVBUkV1c2U0UUduWUtDVTY5OHFleU5Lc0ZTZzRKdkVzWUxjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcXFhcVdMZEVRWTVBUkV1c2U0UUduWUtDVTY5OHFleU5Lc0ZTZzRKdkVzWUxkbWV0YaNoMmE5NmNjNmVqNGNlM2ZjMTdjZGg3ZmMzZTdkM2piMjkwZTMyNDE3aGUwMDFmYzdmajJjOTAwNWY0NmRlbm9uY2VMC3IjW2PprOmuLut7WQGeglhAviUPHHfU+n8H1vyKyatGMS2YvJUvSvBxjC9hQSw7wIVbYAUJIgRr54V6vAifE2O4CvldXb16Qkqh1w9LnhX+A6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ra0o3TTNzUXdOR0hFdEZOZGd4UFV5V1lXdGRWdXhLVXFiWndGdTU0TkVkOThjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdkZVcGhKcGpaVFZHYzRqdjVjWnJWampuV2JQWUM1UHhvVWduenhuVVZzMUJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdkZVcGhKcGpaVFZHYzRqdjVjWnJWampuV2JQWUM1UHhvVWduenhuVVZzMUJkbWV0YaNoMGYzOTk5M2VqNWMwOGI2NzEwNWgzNmFmNTMzNmpiNGU0MWRlZTFmaGMyOWVjZWM1ajdlMGZjMzE4OWZlbm9uY2VMwsoostxej7C8KxTfWQGeglhAABlu2b+acxlXwvhl3Cvr1t6wTRgDZyhwui2K//dXhixO+IBslbAM9a4W7wssVcpaA1M/U91DwRH6JjG8lfwnA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rbW9iNGlDczdXRHdRNU1KNDNOZERzaEplbXF4ZjF4RHBrWnJ0NFFYdUNTaURjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcG1BY0dLdWtLQ281czlzQzhpUGVaajkyMWc3YzRWeVM3WEFNcjR2ZGZSN1BjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcG1BY0dLdWtLQ281czlzQzhpUGVaajkyMWc3YzRWeVM3WEFNcjR2ZGZSN1BkbWV0YaNoMjk3MTk1OTRqM2ZkYjI4MDA3Ymg4M2I4ZTQ2OGpiMDlhMDY3OWQxaGEwNjQzYmI4amE1ZWQ4NjcwYmRlbm9uY2VMBevInlpWjVqplZC8WQGeglhAVuW7Dme4SHo/P7+mB67OfD6DK6NBnZy1cyBAsbk76HL42MjYzPp8fi8+JkoZDs4g24iXi9vixQhfpPG8GfrUAKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcmJUbWRXaDZ2TUg2eDZFNlI3UnNpUEpIdFloM3hlSzdtTnFpSzJoekVxZ0VjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ralZtNUdqblhLU2I4d0s3VDF2dmU4eHRuWlJvaUVYUjJGSGdQYVF5M2lEeWhjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ralZtNUdqblhLU2I4d0s3VDF2dmU4eHRuWlJvaUVYUjJGSGdQYVF5M2lEeWhkbWV0YaNoODU4MzY2ODhqNGZlNzI1Mzg2ZWg4NjNiMTIxOWo2MmY2Nzg1YWFmaGE5Y2M0MTEyajExMjgwZTM5NTBlbm9uY2VMUPFgyqtzEkRPXukGWQGeglhAIHB/6ndHOw4ruRy33I9yJej9A/ec3mGJsIEmNr33/rErTXlcNx+YH269sRCO9xOoRXFugTFDkAr92gjoUAeEAKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rb3U4ZkRxc0xjcXVNaWRlRzZGdlpteFJOdG9EOVU0S2dIWjgycVBxOHJLR25jY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rc0RrenVVMncyeFZocVFGTjJVdHhyWkE1QWlIdWpHeGdEWHBueXdoNGV0TGRjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rc0RrenVVMncyeFZocVFGTjJVdHhyWkE1QWlIdWpHeGdEWHBueXdoNGV0TGRkbWV0YaNoNzIwMzUzNGRqYTMwMDY2Nzk4Y2g5ZDM1ZmUwOWoxZTg0Mzc3MTU0aGE4ZWY4YjM3ajIzMDM5YTljMTdlbm9uY2VMVLWsemG3WIh8/QshWQGeglhAR0zlf5Hc4m8hEwf664w9Cim5PHJftx0Y/rno06f9lTth3JuvY8znsXvl7Ym+ShIjTk9H9uJcjpVaNABfkr2YCKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcUU3SkptM3pkdXoxV2hFenpRcFZQa01SODdRaG5jRlpDNmpnMlZOV2JvVHNjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbTN1ZWF5cXo5akJXa2NRcWI2RDJpbkc1cVk4OTNOVWtnTWdXWmtudU1vcVJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbTN1ZWF5cXo5akJXa2NRcWI2RDJpbkc1cVk4OTNOVWtnTWdXWmtudU1vcVJkbWV0YaNoNmUyNzYwOThqZDQwOGRhYmJjZmhiMmE5ZGVhZGo2MWFjZmNlOWMwaGVhNWRjY2QwamVjYWUyMDVkMmNlbm9uY2VMcrvruHcj5dHWBhjMWQGeglhARe/SIBfmxJzT9pT650Vr3xmTvOhB2vOO3SsxEk5emAbTD0msHSWiwROgpbcsVOczBAMpR3MDlv+KDWzU5vKyBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rZkhTbW12TEppM1R5QmF3emczbmlFWUxOQzZnREd1NW9xcnhueFo5U3FkYk5jY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rblhhaXl1cUdXdTFrWTh2Rm1tZ2lMRjR1YnB4bW8zZlg3RlNpdlhBcmFac1djcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rblhhaXl1cUdXdTFrWTh2Rm1tZ2lMRjR1YnB4bW8zZlg3RlNpdlhBcmFac1dkbWV0YaNoODg2NGQ5MmNqNTZhNjZhYjY5NWg4OGI3NjAwOGpmMjQwM2FkYTExaGZiY2Q1NDQ5amE4ZGVhNzhiMmZlbm9uY2VM2bLoKvbEKP43Vixd \ No newline at end of file diff --git a/pkg/container/testvectors/Base64StdPaddingGzipped b/pkg/container/testvectors/Base64StdPaddingGzipped new file mode 100644 index 0000000..d619ebd --- /dev/null +++ b/pkg/container/testvectors/Base64StdPaddingGzipped @@ -0,0 +1 @@ +OH4sIAAAAAAAA/5zX+ZOUxR3HcY7FwhKToFJGFAU1ghzL8/RztpxzrzsHszuzcy1Ru5/unmd6dubZnXsGE9EtFVnFSCiNEFfFiCSKqIVI8ECBKGiJiMSTFdCYeGDK6GpMWcSUtUOq8ts++Qfev7zq+6nu+5lRys+riKuTYwdvSCw9NrDty6c/Wf668n73nFsqQ988uh1Z8rTNu9X6hkO33S/vG1i92+//ontN6/BubU54fntiUnXqrPNfo9p5bnTWd3PP3IhMt/z52L5i2UD5+aQnvVRsFVqFeQWjVdxsoDKp6SRDrsjS+hUNNZjNtFdJsa1CzUwCxSK8imjO7curtFHwBqy8US3hlBT08XQlkWeGkSPmfGZZ8zEqGLTWOzX9yyemGZli8X+b1XAxESdxALIVX6E3xkiY1coeswLDMOp1BVRgOrGnqxEtukoJo9fqub7fQD09Rmv3z/vRYtZaQT1lOs4olvH/nyU5WkIPmEAQkAYg5lhDqigxjeimJoqEMgC4JipMViURiCYTBQHLBHGZCkyRNcgYzVt5gwauDa67+uiirx+KuQZnjgB9/2H3noUzrj79rQdv27HpuSufnOrfvu3Myf98rfv5Tfds2OX9de9w92Mfm/m9s2eOe/etzPnd/1jdeqI8vvuVbYOb8Q2Ltky0CZRDlAfcJKqHGSklXclQxR+mPKbgcM5n5awkRYmwM2SltSQqjRYo3RFD/lDOnW/TYrxd7ayVQaQme10dVl3RPCardrQHkFJUvTUT2QCyk20CaQpUFF1CnFJJpDqVoGYSDKhuMJVDlaoqpIIMTIZVhcoMcCYAnSgC1GkT6BF9x2d9X19nTBw/+cgI0KO+/W+sk4cmDz7/7AM7D7s2+BdIFw0vGLv9yzm7Vv3h1QN/WZHwxy9Qj0evv2ftkX891BLy7V2cWjY+sGV9f+zYHh4eYxOoXE8E3BFPNB+q0Kw7XexLpfXeXiPrd5WC3N0Wj7iBFfeQMqi06aMFKtcMf9wRrbN4EKOa7KxYfRkQ0/3ecJ/LV/dnkl2OgEPSvIViNmQDyE62CSRDgWEBilwECpWwADVsKkQnhgQZ13QZMVWVITGRCLCmI8ANCWuiSiSqNIF+ExuqnnSds6b7zv4XRoDWzN08c6H1GByE86wXj+561L/q9pc+G1jy4/ya71uPb3kXLJmwe/jlHFu3+NhFK2/8Sce4sZtiLQezq/uVBx6OXnv3n0+zC+T1F5MuENTKFW8u0ld3GcVQNl0gZodSiCk01UkMV9SVauRNXR0tEOtkic5YKtXlDgKt4VFKuhWoGiku417g6wuUimZ7DbTJDAGv2waQnWwTCIka0XVJ4JCKVJGQLGqmIamaYaiQKwpVIFQMgZgUCQLSicqxTKCuyxLATaD4JZf/4mhvbf19y15uGwFyrodHo9vOP7kzObhklvuTfQ/j3IQLn5nz6dKNn0xo7H/nkQs+HT70VvLwmWzvm3c/Ofn7g3f91jvkb7/5vpWHHl8EX3/B7sSxiq9R5bkIsAJKJurhPj9z1GgDh3gy18ViyOP2sa5oUso4C9aoJ84fq/fKKm3LtRe8gWTe568V8z7sMCU1FnA7gJnOhXq9Lk+XaHTamTgb2SaQQIFEEaJcgLIqQEEQgaloGsKIMs5ESRYFQVYlU2UEEVnEXCUSA0zTBLUJJM/nb351ecu/h6dLlREg67y3b3be96Ky/p7L7jzeO9s5Y9Ka5fNxZHc8cVC6+K83iXPfO/GjL5ybrbZrWq4QPu64rfu7adMP7v/uyt/f0r7yJjBpkk2gvgQPOTusdDGqhOuKNxROw0ba29lFUjCbqNV1MSDWZZxgViRkjBbI8jUa1aKl0Y42qz3sTVt11dQSeo5WqsGKD1NY0sN6QS8aUhXaALKTPQWEFU0nKuMi0aGCDayoJjIwZjrGHMoEAUI1QTINxUAShCo3DCwTWdOZ0QRaAhdf1yIf7unm5atGgObsH8rs5FO2wH1r/zb84c8WrNLL179/za+G51WvkvaKXjguP3XmXWetHTix59zd32Te9h2ftGTbhU/dv3PvjcvAlBnjzrAJVOjMUKhKznS4jZR10ZWMVz1JZ5zWHbmuSqnglzpSMqPVuiPgkkcL1Butd1DQVQmGgByp0S5Xo5SLKXlfvD1QDolQZ2oxXIwFk42AUrEBZCfbBBJlSZFFReIIQZlqhgR1U0MSlilROBAMTACBAjYZMyjUich1w1B+2Dd4auIOnLPCY3wFNzxIc+tGgKqX5an27Qtn37C2d+ykS7XPnz30TSi9onDd2eeMK7N4/6x78XLQMnnlm6/mFlbIeRee9qWybfCjBceSi6YVdg2s7ZxgE6ga7sp29QGTJ+LOWAooJcsXJ9loQgvFmZOXOgNt7nox1jBRyAVGC8SNbK0zCyKmrDsDRrrhShLqqGQ1DKJyJVMvknY/jNV52hQbThtAdrJNIIgVSdBljUuCwCSgMxGbBhENJBLIBWYgAzCsAZNAwdAFXeZMVVX4wywKTaBZTxz9E9y6b0JPgu8YAZo9A258I6l75KfJM2Omb+T3zjDPTW393SUXnHjswB3DY05a7wQ7ux9fXjtjx4QxS6fPfmns3598sWf9lkvB83sOXrxytt1XXK5IHCHk1Tx6sc0fTPdlCjTBYmWHHNH8XR2s3khVjKqeFD085R71BeXrdVytWKqSqCaDMiehTm/Q4jCVrUcdrBKTOfR4vEGto01N2bkgG9lTF6RBwFQicAqJALGiaropCwggCWMuAyJThAxETUIA0CVV57qkUEVlOtSbQGMGJuaOTFzyx/FHjj81AvTqwIExd3vB9FuXrdHmrp7106G33/vgo9tP3zjF8t5ZOLz52q2bjt1x6+yTH6zQbmGpiasOJ5Xw+oU7n3v8YMuH77Vs/bbF7kcVmjBmuhuhVMyVUNWiSXg9XMs2yvUkpCnma8BKxlK0eqXT8o0WqBgpmI5YKuLvyLiUeqTsp42S6qiasUCjlFG9Xk1pr3ujpN0BLWoDyE62CSRpMtBFTeaipKqYyZJKTEwURGWkcVHAWDcY0TSTYipjjFUu6YL6w2P8v0AnP45M2du/fWgHeOWD/wQAAP//IuQ6Y1MQAAA= \ No newline at end of file diff --git a/pkg/container/testvectors/Base64URL b/pkg/container/testvectors/Base64URL new file mode 100644 index 0000000..9fe7b78 --- /dev/null +++ b/pkg/container/testvectors/Base64URL @@ -0,0 +1 @@ +CoWZjdG4tdjGKWQGeglhACpXpdxwBGhOBgP0i_jkbddYEXF1SIqsFv3_hqgTujlJB1hiKUArH2AIsVSKtpGH-g1tI691qlTZrWoWbReObC6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raEFHdzlXbk1yMlpoRHlBalpVVFJ0UEZURmh6WXN4SjF2R211NXVEdDJFcFRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rc1FtZVcxb3RUeWNQeWRnQ1hxakxRS2JaRUZSMlF5cXJwYWZUZjVhNWFiYVdjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rc1FtZVcxb3RUeWNQeWRnQ1hxakxRS2JaRUZSMlF5cXJwYWZUZjVhNWFiYVdkbWV0YaNoYzdjOWYzM2VqYTYxMzQ2ZTVhMGhmNzNhNDJjOWplZDU4YWQ0ZTg1aGZlZjI5NTlmajBjY2NmN2I5NjFlbm9uY2VM4c8LJwfjIdKZbUd-WQGeglhAGRizpuAMSDUiVK3a7JS_Wr-Z1lqn9eASP5He4iZi2lsXcqgKOozEHOBddL302HETEZyWvZJpjsr7pRDl8nvcA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raHo4bWtKOTZOQjN5Y1VnNHcyeWVQbzd3cnJwYlVYZkZVNEhyQ2tEaEg0aEJjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1raEZ3SmhiNENaQVBScHlXWmMxNXA5NU04NDE3RW1RbUdIU3liMlRwOW95QVpjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raEZ3SmhiNENaQVBScHlXWmMxNXA5NU04NDE3RW1RbUdIU3liMlRwOW95QVpkbWV0YaNoMTYwMTM4YzZqMmJkYWYzNzcxYWg4M2Y4MTdkM2pjM2IwYjdmNjQ5aDkwZGMxODA5amMxMDZiOTJmYWZlbm9uY2VM0GJFExCefuuIz3DAWQGeglhAKgFiPWomi3iuL4RcE_XMscG8JjAUBCTsCu9_aQY6U6sA_E2GnlI37ASYcnqgM0Kk4BLjta2nHgKh6T26CtY-AaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdkc0aXJlN3J6YmtTYXR1MVl4c0J1R3FLczlrTmdHWnRUWFFCNkhXRm1iUlRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdnlwMWdWNlM3TFhMbUNOUEhRblR4VnBjeFJTNlhuZkdIaExycHVldk4xZ1VjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdnlwMWdWNlM3TFhMbUNOUEhRblR4VnBjeFJTNlhuZkdIaExycHVldk4xZ1VkbWV0YaNoYWE2NmY4NDdqOGQyZTIzNTM3ZWhjMzExYjNkNWozOTZlYTJjZTE5aGQ2NmM3YmVkajg3MGQ1YjNhNmRlbm9uY2VMw0wBN_QOVu5WH3ZUWQGeglhA4uBojvXBeoEJ4qnDmtHrtZArqBOj1jRMQrXOxy75JD2xpBhhxW_c90ofhjexFy5m-TM1a9pciFfVdgUwc8a7AqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1razVKRXNDUW1tWGZoMU5XazJVSjJBZ01zYmRxNDhmU1JoaXdiTmJmN0ZVbVFjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ra0VXanFxRkVwZUJlakJBREpneTNRdmZBU01KSkZjc0FIZmRqNkpkSlJKSnZjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ra0VXanFxRkVwZUJlakJBREpneTNRdmZBU01KSkZjc0FIZmRqNkpkSlJKSnZkbWV0YaNoZGVjOTBjOWFqYjZiNGU2NTRlY2hlMGYyN2VmNGo2YTJhZWZjM2U5aGY1NjNjNjEwajNhM2EyNzA4MDJlbm9uY2VMdQysOoQ6Wn3YZJ9KWQGeglhA58PG4OcNtwHt9nXjgcTBebRvhQhVTG6tYyJvTNeFaJj9V-JD3jiT4ZgLJKFenedmwNA0Et31Jfu6MNqkiYUcCaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ramQ5THQ0WXRKNFU4TjRRc29hUVRveEFqekxlMkZnbVZXUkhycTltQUFmTHVjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbkJITFl3N0tpbzVwb2NhclVoTW9BWmEzb25nZXpjcmJpcHRLRkxGU1REM1RjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbkJITFl3N0tpbzVwb2NhclVoTW9BWmEzb25nZXpjcmJpcHRLRkxGU1REM1RkbWV0YaNoNDNmNmZjNjlqOWUxMGNhM2I5NGg2YjNjMjYxY2pjMzBkZDlmYTI2aGNkMDAwYzU1ajA2YjZjMDNiM2Zlbm9uY2VMbwisskfMLx4-gstgWQGeglhAIgN-nA7mA4g2FKr6_CR3kCPzcegoqgIDWk1qCotQWLo0ntG2WoSfnJvPWj9o1HLtxTAKAWzexFwiBTamow2EA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdm5lcWd0ZG52cHVmdHQ0emJHSEZxMnRMSjRkc1dWN2N2NHhrbjlGajlXUjZjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcmhpc3JqVkFWOVROQ3dkejY4NURpdHY2eWRaSjIzWkh0cUFjM0E4SHJvUUhjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcmhpc3JqVkFWOVROQ3dkejY4NURpdHY2eWRaSjIzWkh0cUFjM0E4SHJvUUhkbWV0YaNoMGM3OGVmMmFqMjI1ZDgxYTg3ZGgxMzIyZWU5MmozZTJjZjNmOWZjaDQ1YjM1MWYzamExYTMzOTU1YzZlbm9uY2VMGkPaJ3crRATPQ7jrWQGeglhAFK5Ex__PUA3vkGgS6JKYwyGf03AvjNKLW7S8aDnC4lRCh0VjOdMQ1xHHsV6fsqBDcPC6dwqpFTHOO9YbMPBNCaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rZkZSRndzVVNNc1hzWVRvWGtWZ01mQnBwbTdxRkpqc2g4Z2FSZjY2Q1FvMmhjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdE5UdUhUakI2bWhhMkNKZFFRQ2FyUFE5cGN5a3RQcE5DVmg0TVhMVlNQNlFjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdE5UdUhUakI2bWhhMkNKZFFRQ2FyUFE5cGN5a3RQcE5DVmg0TVhMVlNQNlFkbWV0YaNoMWU2Zjc4YzVqOWRhOThkYWVjNmg4YjRjYTg1OGo4MjNmY2Y3MjliaGYyN2I1YzA1ajU4NjkyMDEyMWVlbm9uY2VMrIMpUz2MjCvzE0tMWQGeglhAy9s8amUFAKS7soQbA4oFzhU4cVRJ73LhUFicmTM3q5dd3vVfy-xeq7yhc-8orqEvsdiXPRVbfZdFtjmEbjRSCqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raVoxMUxWUm9keU0yRmI2VjlUbmF5M3g4bVhVa3p0R0c3TlVxZHFrOHRYYkFjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcFVKc3J5b2V1Mnp6WVNBdng3Z1VRcG0yc2JyRmVFSnFNVERIYVVnSjhrd3djcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcFVKc3J5b2V1Mnp6WVNBdng3Z1VRcG0yc2JyRmVFSnFNVERIYVVnSjhrd3dkbWV0YaNoMjdiYjAzZGVqOTY2NzgxZTQwOGg0NDFjMGIzMGo0NDhmNzEwZmU0aDc0MmM0NDQ5ajdiZjVjMjhiMjJlbm9uY2VM7ctTlMIv0HNvXrsUWQGeglhAUgkAhE9RqmEC6Yx5EUrhcq61qNc3tJ1x1bfAuPO01gxVkD9v4PebljD1PVtYE8uFgQtMFA0u0oI4mHTBHcx8CKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdmZNNVdxZ1JlWThWN3FLRHdWaWs3NHRLVmhjNktOeEoxcHQ2MTNGMktySnpjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdTV6WmhUajdENTF0azNGRTRNYUtCVm9paU1rRnpGNHdNOFl4SFhybmpUdlZjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdTV6WmhUajdENTF0azNGRTRNYUtCVm9paU1rRnpGNHdNOFl4SFhybmpUdlZkbWV0YaNoNWViOWY3YmJqOGFjYTI4MDhjOGhhZTFmMTA4MWo0ODllM2U1OTk4aGFmOTUwZTU0ajM1ZTMzZDNkMTVlbm9uY2VMn16caMQ_aieJes1jWQGeglhA3zJcxENVVf7UDXPewhqsoEHO6Y2fAjOkpjAjdpz244-w2GUand8PTxp1E7ET8FqFx9gD74WjPbzamIoaWsCzAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ra252UjFjb2hRNzdNaU1mZ3JWelpvdVlGaHk1RTh2YUNXckFvdk1zNXJvZkxjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbmFhZmdwaUU0OHo3WjQ5N1F6NkVXQzI2ZlluRlI4NXJjYm5kV3Q4UFZoVk1jcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbmFhZmdwaUU0OHo3WjQ5N1F6NkVXQzI2ZlluRlI4NXJjYm5kV3Q4UFZoVk1kbWV0YaNoMmViYjFjMmZqYmE5ZGE1OGFkM2g2Yzk3OWU1YWoxNzM3YjBmN2Q4aDkxNGM3NDE5ajZjMGE4ODhkNzBlbm9uY2VMgiJAdle-huUpDQ2L \ No newline at end of file diff --git a/pkg/container/testvectors/Base64URLGzip b/pkg/container/testvectors/Base64URLGzip new file mode 100644 index 0000000..4c43905 --- /dev/null +++ b/pkg/container/testvectors/Base64URLGzip @@ -0,0 +1 @@ +PH4sIAAAAAAAA_5zWa5cUxR3HcVcRiejmuCII3siKwgnubnd1dVW1eIS5rzM9w8zs3HZETFd19_T07PTcZ2cH1KMgoB41YgIo3nM0G1GCRhCRiILIEaIhIEo0Bi9c9ewaFfEazYMd8iCPdvIGvk8-p_71e1RnZaujyt_e2_LQLYk5Fbb1QefFR9-_5LzWO59-9PfWoWV_nN_f177z06eH31j13Je8-Wpk35bTccKjtV_22MRfnz9DufuSg8Ntn02KzZtw83sLxj-mGE441FIoVZhidal9qTl8J9fJdRRZJz_IlIpaI2pavTKjDVxZR_5MQXa4aFIFgr-gMCrrOTWUqWNajNZERzin0GLRz7IxsTcfAgnGsqrRpedyXVQpMq2Wn5K68dmLWLpU-p9msRbQ-usuHEsGorZK0BbwxWzxvCdrhDK2vL23UMCRTM0RlStVxPK5vpsXMaWvj3Vee90i5Wq9s6r0VbRTWalC__-smtXKyu8MERNOwJSZkCACeAFgzkC6xGkYAhOLqkJEiBXJkJAuCUBUTMIA4KEgAEWzchbT5DXHfxwc7LiqNnPmRwdHgBa8NO6jDQ9uvW6wa7DHYetfP3DxO4s7JhybLa1YcerUV-Yae7aDww_P2_zjvH9euWqOOW3yjBWtO2__bKzLvfnR_enEpmaBMtFuy4qEsgrlqRMpPm86mql6apjI4V63V_bFgl5WT-USpQhDowVK2amiFmM8tEUsiRJcCeowHM9AvVQreLGE83LMkcBBI99dAE0ANZNtAGFewJjwkilBJPG8TphoKEQhmihKpsgBXuIVhXEGRRQpRGAmhzkRqIpOaQNo4afuoTWTj60cfjw5fQQoxy98ZPGmF3YsWLJo8voJ587Y9-X38geyLBq_nHXZIy9fPun5m1pebqVzv7J-NuaM08XEzYl3v3kmtfuCr2Yc-VAa3vtxS7NAnpKSKsBEzVaMphWdFB0kgNSEgHpwiPe7a0qaxYtVG_SVq_7RAhkey-asYmfUZ1XKMbGSr-Vo1UZ60rZyNV6Uy8l8MY_iES1ic0abAGom2wCCEqcLVFRNpEKBaghAaCBJUYgKOVNVGMQCUyXRQBqFTJWwKQkIIoZFQW8AvZV64bRDez_6flYbbmu8oDa_euu2yJB9w1vX3uO-YrZvobj2yK07V7-1YebjrRcgbsvS4fiyJ9_gpx4KvT5n_A7PTz-cqF_asuuaoTFnn2N-bbU2CaSHi0bJlrEi3kyh6NFrgqOu59VKOJ2t9Xj4sllT4wXqJdFQfzI2aiBvIGSG_Zls2IsDSdKblRV3QlQz2X7Z2evudVBk16QIM1yiCZsBaiLbAOIEnROQppu8AjgKiKqIhgQVwDQCTCKJkqCIBIqGpoicCilvcoxDWJewcPLE3TD0fHzb0y_BrtRd2gjQN3fd--36ju_CYzuV8adcsOeA95UvVv-09N1t05_bN7DllouWTN-18r4nTm-9Y8uZ2L02fj1XLizHlzyfrZ-x65Pt503b0uyJs0oDhpWuSh4vwFpJjlcDciKRCbtQTzmvxkWrKIWCUQm51FomO1ogKyMLyXSPx0xHxKrixwFT7a9XgzUUTuedXgDi8XKmgGJmJOwMNQHUTLYBhERGkE6YCTXEYYAYxxuE5xGnCbopAkWjAtCgbhCMKAUQmQqkvA4wJaQBpNyhPfDyK9Ijs-yOv40ArTv_hoMtSbB12NjtzD1Ef35ow47k_D3frn41f-2LX6YmdTxzzba3t180_7XFV5m-fz304tj2Td-t7Nx_oblszv7ctI2Hmz1xhayl1myOkhEOxUt2S_S4shFvMI7dIbseyPa4AlalJ8q6g2KhGBktUJGVfQEaSsXtsK7EnXnC6zIP84ZHrYUyGR9vcxXj9lwuaIvG7E0ANZM9-QcBHkNCNFMFEKhYFTRiSBoUOV0RTE3DKmWMCJKhijzgeJWakgoIwyLUuAZQSlq-Y177mBOSHt49AjRF-PBAe9d1H3ffcvyZczrHLAhP9X3jC5y797YFa-mNxYl_evDqo4fu_8NZh3d88un1R1f_5cfBf8_76p3tv_psyooll_k-nj2mWaC6b8BCmVrCytYjIFZKF1wytmUFKdijVuoCLIdrBV8dk3AxbIwWqJRAkpyRMnkY9ReLcipX8PR3u6RcrheykFINGCCc7-lJ25LMpjUB1Ez25IljDHFYpCYUiUYR4yVkUMKYIEiSiTDBSFcRgAajPAOU6ibjmShBToOwAXRf-6V31zceuXHdjHV9I0D7Jn7wgXzr4TVX_-PYw_mpLatO69_qW7R1Q-m8o9OcsGP2rM_f3lM_3n0_-aL18zWHV-eHzkik9r4_LriMt16a98Tm889sdiTASDqRwFLW3evj6_mU6i96cqInGEY4Ea0F1brlsPUXJSlCrcpogfRCzGll1YKRsqXrWEuVajq21wK5buKv5FxOV7wQy4Wz5Wwg3NRIaCbbABKopoi6BkymSSrTRCpSQ8UqEEQBmkTTBQp0VcSGBgAvYaiZCgQMUiz89wXdVtm7cNtjqvLmb5ZtHAHq2vfX9Uem2q-ZXTzw3oub0ruemj52f29hwuWeZ2_6_qylidcO5d9p3310-_wptTV3Dv1WXZzbuLsNDU2-dzDy7Sf84MxxTQKZAxWi5wO0Agci0WTOnStHklUpbPaHSi7JcsV9UbvH6_IHzHTVNeqRwLtlj6rV_T3ZehxGex2KLA3EXX4XhNmaGZdj-VjCRgy37HA4mhkJTWQbQBImhOkcbyIeQA1oiOcNBVBEdBWYlOc0IgBAREPnBYoABSavUAI0RhFuAM22T_nzs8NTTyz94YFVI0Bze_9-4utz3dvfW5B7fXL7uJWvr-1rO-uG5evusY_jlk-ynvzheOfE5IFIt4ieunAfO3bK-CWuX2w-0HbQP_fDi-8Y2NPsitNYLCfr2YLGEhrwg7LNoL3ZWKLSAzwDxNvNV_OA1L0oFVdToz5xuURswBupyoo7Ggua8Vg8kIjV5TLvcyCUDJsV7PXKuglTehn5mwBqJnvyBSEd6qpKTV5HCEOeU4mBCJSwpImmIkBd4BRGoUEUHUlMQ6amQVVVJB2CBpBwxRsH-9-cvnYnOfvd_wQAAP__hMJ751MQAAA \ No newline at end of file diff --git a/pkg/container/testvectors/Bytes b/pkg/container/testvectors/Bytes new file mode 100644 index 0000000000000000000000000000000000000000..32c6d7ebf0c58d879d4e70b572404ed0ab735277 GIT binary patch literal 4180 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 G5C8y~7K5t* literal 0 HcmV?d00001 From 9cbec37685a9d893fae14b768c54487f6e21d5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 8 Jan 2025 21:14:30 +0100 Subject: [PATCH 5/5] container: no extra keys in the packaging allowed --- pkg/container/SPEC.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/container/SPEC.md b/pkg/container/SPEC.md index 9f92720..7cd6d5d 100644 --- a/pkg/container/SPEC.md +++ b/pkg/container/SPEC.md @@ -27,7 +27,9 @@ The UCAN spec itself is transport agnostic. This specification describes how to UCAN tokens, regardless of their kind ([Delegation], [Invocation], [Revocation], [Promise]) MUST be first signed and serialized into DAG-CBOR bytes according to their respective specification. As the token's CID is not part of the serialized container, any CID returned by this operation is to be ignored. -All the tokens' bytes MUST be assembled in a [CBOR] array, which is then inserted as the value under the `ctn-v1` string key, in a CBOR map. The ordering of tokens in the array MUST NOT matter. Also, this array SHOULD NOT have duplicate entries. +All the tokens' bytes MUST be assembled in a [CBOR] array. The ordering of tokens in the array MUST NOT matter. This array SHOULD NOT have duplicate entries. + +That array is then inserted as the value under the `ctn-v1` string key, in a CBOR map. There MUST NOT be other keys. For clarity, the CBOR shape is given below: