From 69d5c49283e162321354d760717714cd4aa2626d Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 29 Nov 2023 16:52:49 +0800 Subject: [PATCH] Squashed commit of the following: commit 92406502fb0483cbeae370fb9095fb44067ed8a2 Merge: 0c1ec3b 4198690 Author: Junjie Gao Date: Wed Aug 9 17:07:34 2023 +0800 Merge pull request #1 from JeyJeyGao/feat/ans1 feat: convert BER to DER commit 419869027fb86f673f7c6d1c3c0031756aab1c6a Author: Junjie Gao Date: Wed Aug 9 09:14:29 2023 +0800 fix: simplify code Signed-off-by: Junjie Gao commit 75ce02d759d624645982c7fff493e672b7206ed5 Author: Junjie Gao Date: Mon Aug 7 20:33:08 2023 +0800 fix: added Conetent method for value interface Signed-off-by: Junjie Gao commit 7b823a9065a262ccc8c3226b139a1570f9d4cedf Author: Junjie Gao Date: Mon Aug 7 08:54:37 2023 +0800 fix: update code Signed-off-by: Junjie Gao commit 41ecec67b96772ff8db4e3378c953c64e83e8880 Author: Junjie Gao Date: Sun Aug 6 17:33:19 2023 +0800 fix: remove recusive call for encode() Signed-off-by: Junjie Gao commit 8f1a2af3c061df99f10edba3625569d788638261 Author: Junjie Gao Date: Fri Aug 4 13:40:09 2023 +0800 fix: remove unused value Signed-off-by: Junjie Gao commit 9b6a0c526189ea04d42a42da07e9c2349ec4c33a Author: Junjie Gao Date: Thu Aug 3 20:25:22 2023 +0800 fix: update code Signed-off-by: Junjie Gao commit 91a369137df03d255cf25894839c787ce9ce7785 Author: Junjie Gao Date: Thu Aug 3 20:11:28 2023 +0800 fix: create pointer instead of value to improve performance Signed-off-by: Junjie Gao commit 1465e3e1bb07a4de9f25c9b9fc89debdad34abe4 Author: Junjie Gao Date: Thu Aug 3 20:04:44 2023 +0800 fix: update code Signed-off-by: Junjie Gao commit 6524a9ce6f7363edcb001322815dd2e760fd361e Author: Junjie Gao Date: Thu Aug 3 19:53:27 2023 +0800 fix: update variable naming Signed-off-by: Junjie Gao commit 6cfbd9c03583d35fa8821f647d36c3d63f462d31 Author: Junjie Gao Date: Thu Aug 3 19:47:39 2023 +0800 fix: update code Signed-off-by: Junjie Gao commit b9c73bd63f4237480a299056df168d3099fd04d0 Author: Junjie Gao Date: Thu Aug 3 17:56:52 2023 +0800 fix: update to use rawContent instead of expectedLen Signed-off-by: Junjie Gao commit 3c994028abfb250a3eaa87b295548af7c21b902b Author: Junjie Gao Date: Thu Aug 3 16:45:09 2023 +0800 fix: update comment Signed-off-by: Junjie Gao commit f4dc95f6a97ca31a2ebe963a7844a2c0b6ef87c8 Author: Junjie Gao Date: Thu Aug 3 16:41:57 2023 +0800 fix: resolve comment Signed-off-by: Junjie Gao commit f91631640843e1dd48a3ce13bb7b06e73a16429b Author: Junjie Gao Date: Thu Aug 3 16:40:37 2023 +0800 fix: update code Signed-off-by: Junjie Gao commit 22afdf81b95eb45e4767d61b35e6fe1218d8d248 Author: Junjie Gao Date: Thu Aug 3 16:34:34 2023 +0800 fix: resolve comment Signed-off-by: Junjie Gao commit edb729cb1628ba2c514abfeebd02cd002fa618a0 Author: Junjie Gao Date: Thu Aug 3 16:32:47 2023 +0800 fix: resolve comment Signed-off-by: Junjie Gao commit a8ba0ff99ce35a252af4e30c9b64927d27d11354 Author: Junjie Gao Date: Thu Aug 3 16:26:29 2023 +0800 fix: update code Signed-off-by: Junjie Gao commit bc18cae59daa3700b49503c46bd6555634c599e5 Author: Junjie Gao Date: Thu Aug 3 16:14:57 2023 +0800 fix: resolve comments Signed-off-by: Junjie Gao commit 643f3886ebfb1f75af4b3572d4dc93bbdc151f3c Author: Junjie Gao Date: Thu Aug 3 09:17:39 2023 +0800 fix: update comment Signed-off-by: Junjie Gao commit b5d5131b5ebf32b6901d41546b7dac75d09f9b5b Author: Junjie Gao Date: Thu Aug 3 09:15:23 2023 +0800 fix: expectedLen == 0 should continue Signed-off-by: Junjie Gao commit 234574046999798f70cdead8232446b71bb23ff1 Author: Junjie Gao Date: Wed Aug 2 13:01:38 2023 +0800 fix: added copyright Signed-off-by: Junjie Gao commit 936ba2bc9a578b8ace72b4976e561f05213860e1 Author: Junjie Gao Date: Wed Aug 2 11:36:02 2023 +0800 fix: remove recusive decoding Signed-off-by: Junjie Gao commit 4fd944a74330254fc3e9805cd349e40b3afebc86 Author: Junjie Gao Date: Tue Aug 1 21:50:10 2023 +0800 fix: remove readOnlySlice Signed-off-by: Junjie Gao commit efa75756326adcf8b1bb2cfedd2b31c4d7a9f5e6 Author: Junjie Gao Date: Tue Aug 1 09:38:57 2023 +0800 fix: update decodeIdentifier function name Signed-off-by: Junjie Gao commit cbce4c135f14caa3ca73ea7ba5680208c62ef9e7 Author: Junjie Gao Date: Tue Aug 1 09:25:34 2023 +0800 fix: update code Signed-off-by: Junjie Gao commit 45480e5e93508e433fffe6b23ee3a6685277b3aa Author: Junjie Gao Date: Mon Jul 31 21:22:20 2023 +0800 fix: update code Signed-off-by: Junjie Gao commit b3de155b8866f3ceb0a0e6b8ff258ef036efee73 Author: Junjie Gao Date: Mon Jul 31 20:51:48 2023 +0800 fix: set non-exportable type Signed-off-by: Junjie Gao commit 5dea9e5d1c3e44b8837928c164833df4c6a9a464 Author: Junjie Gao Date: Mon Jul 31 20:44:50 2023 +0800 feat: asn.1 first version Signed-off-by: Junjie Gao Signed-off-by: Junjie Gao --- internal/encoding/asn1/asn1.go | 247 ++++++++++++++++++++++++++ internal/encoding/asn1/asn1_test.go | 81 +++++++++ internal/encoding/asn1/common.go | 52 ++++++ internal/encoding/asn1/constructed.go | 43 +++++ internal/encoding/asn1/primitive.go | 41 +++++ 5 files changed, 464 insertions(+) create mode 100644 internal/encoding/asn1/asn1.go create mode 100644 internal/encoding/asn1/asn1_test.go create mode 100644 internal/encoding/asn1/common.go create mode 100644 internal/encoding/asn1/constructed.go create mode 100644 internal/encoding/asn1/primitive.go diff --git a/internal/encoding/asn1/asn1.go b/internal/encoding/asn1/asn1.go new file mode 100644 index 00000000..28d79049 --- /dev/null +++ b/internal/encoding/asn1/asn1.go @@ -0,0 +1,247 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package asn1 decodes BER-encoded ASN.1 data structures and encodes in DER. +// Note: DER is a subset of BER. +// Reference: http://luca.ntop.org/Teaching/Appunti/asn1.html +package asn1 + +import ( + "bytes" + "encoding/asn1" +) + +// Common errors +var ( + ErrEarlyEOF = asn1.SyntaxError{Msg: "early EOF"} + ErrTrailingData = asn1.SyntaxError{Msg: "trailing data"} + ErrUnsupportedLength = asn1.StructuralError{Msg: "length method not supported"} + ErrUnsupportedIndefiniteLength = asn1.StructuralError{Msg: "indefinite length not supported"} +) + +// value represents an ASN.1 value. +type value interface { + // EncodeMetadata encodes the identifier and length in DER to the buffer. + EncodeMetadata(*bytes.Buffer) error + + // EncodedLen returns the length in bytes of the encoded data. + EncodedLen() int + + // Content returns the content of the value. + // For primitive values, it returns the content octets. + // For constructed values, it returns nil because the content is + // the data of all members. + Content() []byte +} + +// ConvertToDER converts BER-encoded ASN.1 data structures to DER-encoded. +func ConvertToDER(ber []byte) ([]byte, error) { + flatValues, err := decode(ber) + if err != nil { + return nil, err + } + + // get the total length from the root value and allocate a buffer + buf := bytes.NewBuffer(make([]byte, 0, flatValues[0].EncodedLen())) + for _, v := range flatValues { + if err = v.EncodeMetadata(buf); err != nil { + return nil, err + } + + if content := v.Content(); content != nil { + // primitive value + _, err = buf.Write(content) + if err != nil { + return nil, err + } + } + } + + return buf.Bytes(), nil +} + +// decode decodes BER-encoded ASN.1 data structures. +// +// r is the input byte slice. +// The returned value, which is the flat slice of ASN.1 values, contains the +// nodes from a depth-first traversal. To get the DER of `r`, encode the values +// in the returned slice in order. +func decode(r []byte) ([]value, error) { + // prepare the first value + identifier, content, r, err := decodeMetadata(r) + if err != nil { + return nil, err + } + if len(r) != 0 { + return nil, ErrTrailingData + } + + // primitive value + if isPrimitive(identifier) { + return []value{&primitiveValue{ + identifier: identifier, + content: content, + }}, nil + } + + // constructed value + rootConstructed := &constructedValue{ + identifier: identifier, + rawContent: content, + } + flatValues := []value{rootConstructed} + + // start depth-first decoding with stack + valueStack := []*constructedValue{rootConstructed} + for len(valueStack) > 0 { + stackLen := len(valueStack) + // top + node := valueStack[stackLen-1] + + // check that the constructed value is fully decoded + if len(node.rawContent) == 0 { + // calculate the length of the members + for _, m := range node.members { + node.length += m.EncodedLen() + } + // pop + valueStack = valueStack[:stackLen-1] + continue + } + + // decode the next member of the constructed value + identifier, content, node.rawContent, err = decodeMetadata(node.rawContent) + if err != nil { + return nil, err + } + if isPrimitive(identifier) { + // primitive value + primitiveNode := &primitiveValue{ + identifier: identifier, + content: content, + } + node.members = append(node.members, primitiveNode) + flatValues = append(flatValues, primitiveNode) + } else { + // constructed value + constructedNode := &constructedValue{ + identifier: identifier, + rawContent: content, + } + node.members = append(node.members, constructedNode) + + // add a new constructed node to the stack + valueStack = append(valueStack, constructedNode) + flatValues = append(flatValues, constructedNode) + } + } + return flatValues, nil +} + +// decodeMetadata decodes the metadata of a BER-encoded ASN.1 value. +// +// r is the input byte slice. +// The first return value is the identifier octets. +// The second return value is the content octets. +// The third return value is the subsequent octets after the value. +func decodeMetadata(r []byte) ([]byte, []byte, []byte, error) { + identifier, r, err := decodeIdentifier(r) + if err != nil { + return nil, nil, nil, err + } + contentLen, r, err := decodeLength(r) + if err != nil { + return nil, nil, nil, err + } + + if contentLen > len(r) { + return nil, nil, nil, ErrEarlyEOF + } + return identifier, r[:contentLen], r[contentLen:], nil +} + +// decodeIdentifier decodes decodeIdentifier octets. +// +// r is the input byte slice. +// The first return value is the identifier octets. +// The second return value is the subsequent value after the identifiers octets. +func decodeIdentifier(r []byte) ([]byte, []byte, error) { + if len(r) < 1 { + return nil, nil, ErrEarlyEOF + } + offset := 0 + b := r[offset] + offset++ + + // high-tag-number form + if b&0x1f == 0x1f { + for offset < len(r) && r[offset]&0x80 == 0x80 { + offset++ + } + if offset >= len(r) { + return nil, nil, ErrEarlyEOF + } + offset++ + } + return r[:offset], r[offset:], nil +} + +// decodeLength decodes length octets. +// Indefinite length is not supported +// +// r is the input byte slice. +// The first return value is the length. +// The second return value is the subsequent value after the length octets. +func decodeLength(r []byte) (int, []byte, error) { + if len(r) < 1 { + return 0, nil, ErrEarlyEOF + } + offset := 0 + b := r[offset] + offset++ + + if b < 0x80 { + // short form + return int(b), r[offset:], nil + } else if b == 0x80 { + // Indefinite-length method is not supported. + return 0, nil, ErrUnsupportedIndefiniteLength + } + + // long form + n := int(b & 0x7f) + if n > 4 { + // length must fit the memory space of the int type. + return 0, nil, ErrUnsupportedLength + } + if offset+n >= len(r) { + return 0, nil, ErrEarlyEOF + } + var length uint64 + for i := 0; i < n; i++ { + length = (length << 8) | uint64(r[offset]) + offset++ + } + + // length must fit the memory space of the int32. + if (length >> 31) > 0 { + return 0, nil, ErrUnsupportedLength + } + return int(length), r[offset:], nil +} + +// isPrimitive returns true if the first identifier octet is marked +// as primitive. +func isPrimitive(identifier []byte) bool { + return identifier[0]&0x20 == 0 +} diff --git a/internal/encoding/asn1/asn1_test.go b/internal/encoding/asn1/asn1_test.go new file mode 100644 index 00000000..90ca3aea --- /dev/null +++ b/internal/encoding/asn1/asn1_test.go @@ -0,0 +1,81 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asn1 + +import ( + "encoding/asn1" + "reflect" + "testing" +) + +func TestConvertToDER(t *testing.T) { + type data struct { + Type asn1.ObjectIdentifier + Value []byte + } + + want := data{ + Type: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}, + Value: []byte{ + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, + 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, + }, + } + + ber := []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2e, + + // Type identifier + 0x06, + // Type length + 0x09, + // Type content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, + + // Value identifier + 0x04, + // Value length in BER + 0x81, 0x20, + // Value content + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, + 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, + } + + der, err := ConvertToDER(ber) + if err != nil { + t.Errorf("ConvertToDER() error = %v", err) + return + } + + var got data + rest, err := asn1.Unmarshal(der, &got) + if err != nil { + t.Errorf("Failed to decode converted data: %v", err) + return + } + if len(rest) > 0 { + t.Errorf("Unexpected rest data: %v", rest) + return + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got = %v, want %v", got, want) + } +} diff --git a/internal/encoding/asn1/common.go b/internal/encoding/asn1/common.go new file mode 100644 index 00000000..eb93ba78 --- /dev/null +++ b/internal/encoding/asn1/common.go @@ -0,0 +1,52 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asn1 + +import ( + "io" +) + +// encodeLength encodes length octets in DER. +func encodeLength(w io.ByteWriter, length int) error { + // DER restriction: short form must be used for length less than 128 + if length < 0x80 { + return w.WriteByte(byte(length)) + } + + // DER restriction: long form must be encoded in the minimum number of octets + lengthSize := encodedLengthSize(length) + err := w.WriteByte(0x80 | byte(lengthSize-1)) + if err != nil { + return err + } + for i := lengthSize - 1; i > 0; i-- { + if err = w.WriteByte(byte(length >> (8 * (i - 1)))); err != nil { + return err + } + } + return nil +} + +// encodedLengthSize gives the number of octets used for encoding the length. +func encodedLengthSize(length int) int { + if length < 0x80 { + return 1 + } + + lengthSize := 1 + for ; length > 0; lengthSize++ { + length >>= 8 + } + return lengthSize +} diff --git a/internal/encoding/asn1/constructed.go b/internal/encoding/asn1/constructed.go new file mode 100644 index 00000000..409fd5a4 --- /dev/null +++ b/internal/encoding/asn1/constructed.go @@ -0,0 +1,43 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asn1 + +import "bytes" + +// constructedValue represents a value in constructed encoding. +type constructedValue struct { + identifier []byte + length int + members []value + rawContent []byte // the raw content of BER +} + +// EncodeMetadata encodes the constructed value to the value writer in DER. +func (v *constructedValue) EncodeMetadata(w *bytes.Buffer) error { + _, err := w.Write(v.identifier) + if err != nil { + return err + } + return encodeLength(w, v.length) +} + +// EncodedLen returns the length in bytes of the encoded data. +func (v *constructedValue) EncodedLen() int { + return len(v.identifier) + encodedLengthSize(v.length) + v.length +} + +// Content returns the content of the value. +func (v *constructedValue) Content() []byte { + return nil +} diff --git a/internal/encoding/asn1/primitive.go b/internal/encoding/asn1/primitive.go new file mode 100644 index 00000000..0c2cdec6 --- /dev/null +++ b/internal/encoding/asn1/primitive.go @@ -0,0 +1,41 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asn1 + +import "bytes" + +// primitiveValue represents a value in primitive encoding. +type primitiveValue struct { + identifier []byte + content []byte +} + +// EncodeMetadata encodes the primitive value to the value writer in DER. +func (v *primitiveValue) EncodeMetadata(w *bytes.Buffer) error { + _, err := w.Write(v.identifier) + if err != nil { + return err + } + return encodeLength(w, len(v.content)) +} + +// EncodedLen returns the length in bytes of the encoded data. +func (v *primitiveValue) EncodedLen() int { + return len(v.identifier) + encodedLengthSize(len(v.content)) + len(v.content) +} + +// Content returns the content of the value. +func (v *primitiveValue) Content() []byte { + return v.content +}