From 62aa8fcef51794f45df9fd4f384cf81c5e686594 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 29 Nov 2023 16:52:49 +0800 Subject: [PATCH 001/110] 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 +} From eef857954ebaa2ad1bdb944ee5f11441d939bd02 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 7 Dec 2023 15:41:07 +0800 Subject: [PATCH 002/110] test: add unit test Signed-off-by: Junjie Gao --- internal/encoding/asn1/asn1.go | 74 ++++-- internal/encoding/asn1/asn1_test.go | 333 ++++++++++++++++++++++---- internal/encoding/asn1/common.go | 6 + internal/encoding/asn1/common_test.go | 86 +++++++ 4 files changed, 432 insertions(+), 67 deletions(-) create mode 100644 internal/encoding/asn1/common_test.go diff --git a/internal/encoding/asn1/asn1.go b/internal/encoding/asn1/asn1.go index 28d79049..8e58294e 100644 --- a/internal/encoding/asn1/asn1.go +++ b/internal/encoding/asn1/asn1.go @@ -12,8 +12,14 @@ // 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 +// Note: +// - DER is a subset of BER. +// - Indefinite length is not supported. +// - The length of the encoded data must fit the memory space of the int type (4 bytes). +// +// Reference: +// - http://luca.ntop.org/Teaching/Appunti/asn1.html +// - ISO/IEC 8825-1 package asn1 import ( @@ -71,11 +77,18 @@ func ConvertToDER(ber []byte) ([]byte, error) { } // 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 +// To get the DER of `r`, encode the values // in the returned slice in order. +// +// Parameters: +// r - The input byte slice. +// +// Return: +// []value - The returned value, which is the flat slice of ASN.1 values, +// contains the nodes from a depth-first traversal. +// error - An error that can occur during the decoding process. +// +// Reference: ISO/IEC 8825-1: 8.1.1.3 func decode(r []byte) ([]value, error) { // prepare the first value identifier, content, r, err := decodeMetadata(r) @@ -150,11 +163,21 @@ func decode(r []byte) ([]value, error) { // 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. +// Parameters: +// r - The input byte slice. +// +// Return: +// []byte - The identifier octets. +// []byte - The content octets. +// []byte - The subsequent octets after the value. +// error - An error that can occur during the decoding process. +// +// Reference: ISO/IEC 8825-1: 8.1.1.3 func decodeMetadata(r []byte) ([]byte, []byte, []byte, error) { + // structure of an encoding (primitive or constructed) + // +----------------+----------------+----------------+ + // | identifier | length | content | + // +----------------+----------------+----------------+ identifier, r, err := decodeIdentifier(r) if err != nil { return nil, nil, nil, err @@ -172,9 +195,15 @@ func decodeMetadata(r []byte) ([]byte, []byte, []byte, error) { // 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. +// Parameters: +// r - The input byte slice from which the identifier octets are to be decoded. +// +// Returns: +// []byte - The identifier octets decoded from the input byte slice. +// []byte - The remaining part of the input byte slice after the identifier octets. +// error - An error that can occur during the decoding process. +// +// Reference: ISO/IEC 8825-1: 8.1.2 func decodeIdentifier(r []byte) ([]byte, []byte, error) { if len(r) < 1 { return nil, nil, ErrEarlyEOF @@ -184,6 +213,7 @@ func decodeIdentifier(r []byte) ([]byte, []byte, error) { offset++ // high-tag-number form + // Reference: ISO/IEC 8825-1: 8.1.2.4 if b&0x1f == 0x1f { for offset < len(r) && r[offset]&0x80 == 0x80 { offset++ @@ -199,9 +229,15 @@ func decodeIdentifier(r []byte) ([]byte, []byte, error) { // 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. +// Parameters: +// r - The input byte slice from which the length octets are to be decoded. +// +// Returns: +// int - The length decoded from the input byte slice. +// []byte - The remaining part of the input byte slice after the length octets. +// error - An error that can occur during the decoding process. +// +// Reference: ISO/IEC 8825-1: 8.1.3 func decodeLength(r []byte) (int, []byte, error) { if len(r) < 1 { return 0, nil, ErrEarlyEOF @@ -212,16 +248,19 @@ func decodeLength(r []byte) (int, []byte, error) { if b < 0x80 { // short form + // Reference: ISO/IEC 8825-1: 8.1.3.4 return int(b), r[offset:], nil } else if b == 0x80 { // Indefinite-length method is not supported. + // Reference: ISO/IEC 8825-1: 8.1.3.6.1 return 0, nil, ErrUnsupportedIndefiniteLength } // long form + // Reference: ISO/IEC 8825-1: 8.1.3.5 n := int(b & 0x7f) if n > 4 { - // length must fit the memory space of the int type. + // length must fit the memory space of the int type (4 bytes). return 0, nil, ErrUnsupportedLength } if offset+n >= len(r) { @@ -242,6 +281,7 @@ func decodeLength(r []byte) (int, []byte, error) { // isPrimitive returns true if the first identifier octet is marked // as primitive. +// Reference: ISO/IEC 8825-1: 8.1.2.5 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 index 90ca3aea..3cc94795 100644 --- a/internal/encoding/asn1/asn1_test.go +++ b/internal/encoding/asn1/asn1_test.go @@ -14,68 +14,301 @@ package asn1 import ( - "encoding/asn1" + "fmt" "reflect" "testing" ) func TestConvertToDER(t *testing.T) { - type data struct { - Type asn1.ObjectIdentifier - Value []byte - } + testData := []struct { + name string + ber []byte + der []byte + expectError bool + }{ + { + name: "Constructed value", + 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: []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2d, - 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, + // Type identifier + 0x06, + // Type length + 0x09, + // Type content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, + + // Value identifier + 0x04, + // Value length in BER + 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, + }, + expectError: false, }, - } + { + name: "Primitive value", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0x20, + // length + 0x81, 0x01, + // content + 0x01, + }, + der: []byte{ + // Primitive value + // identifier + 0x1f, 0x20, + // length + 0x01, + // content + 0x01, + }, + expectError: false, + }, + { + name: "Constructed value in constructed value", + ber: []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2d, - ber := []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2e, + // Constructed value identifier + 0x26, + // Type length + 0x2b, - // 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, 0x28, + // Value content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 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: []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2c, - // 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, - } + // Constructed value identifier + 0x26, + // Type length + 0x2a, - der, err := ConvertToDER(ber) - if err != nil { - t.Errorf("ConvertToDER() error = %v", err) - return - } + // Value identifier + 0x04, + // Value length in BER + 0x28, + // Value content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 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, + }, + expectError: false, + }, + { + name: "empty", + ber: []byte{}, + der: []byte{}, + expectError: true, + }, + { + name: "identifier high tag number form", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x81, 0x01, + // content + 0x01, + }, + der: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x01, + // content + 0x01, + }, + expectError: false, + }, + { + name: "EOF for identifier high tag number form", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, + }, + der: []byte{}, + expectError: true, + }, + { + name: "EOF for length", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + }, + der: []byte{}, + expectError: true, + }, + { + name: "Unsupport indefinite-length", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x80, + }, + der: []byte{}, + expectError: true, + }, + { + name: "length greater than 4 bytes", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x85, + }, + der: []byte{}, + expectError: true, + }, + { + name: "long form length EOF ", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x84, + }, + der: []byte{}, + expectError: true, + }, + { + name: "length greater > int32", + ber: append([]byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x84, 0xFF, 0xFF, 0xFF, 0xFF, + }, make([]byte, 0xFFFFFFFF)...), + der: []byte{}, + expectError: true, + }, + { + name: "length greater than content", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x02, + }, + der: []byte{}, + expectError: true, + }, + { + name: "trailing data", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x02, + // content + 0x01, 0x02, 0x03, + }, + der: []byte{}, + expectError: true, + }, + { + name: "EOF in constructed value", + ber: []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2c, - 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 + // Constructed value identifier + 0x26, + // Type length + 0x2b, + + // Value identifier + 0x04, + // Value length in BER + 0x81, 0x28, + // Value content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 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, + }, + expectError: true, + }, } - if !reflect.DeepEqual(got, want) { - t.Errorf("got = %v, want %v", got, want) + + for _, tt := range testData { + der, err := ConvertToDER(tt.ber) + fmt.Printf("DER: %x\n", der) + if !tt.expectError && err != nil { + t.Errorf("ConvertToDER() error = %v, but expect no error", err) + return + } + if tt.expectError && err == nil { + t.Errorf("ConvertToDER() error = nil, but expect error") + } + + if !tt.expectError && !reflect.DeepEqual(der, tt.der) { + t.Errorf("got = %v, want %v", der, tt.der) + } } } diff --git a/internal/encoding/asn1/common.go b/internal/encoding/asn1/common.go index eb93ba78..7c7110e7 100644 --- a/internal/encoding/asn1/common.go +++ b/internal/encoding/asn1/common.go @@ -18,7 +18,12 @@ import ( ) // encodeLength encodes length octets in DER. +// Reference: ISO/IEC 8825-1: 10.1 func encodeLength(w io.ByteWriter, length int) error { + if length < 0 { + return ErrUnsupportedLength + } + // DER restriction: short form must be used for length less than 128 if length < 0x80 { return w.WriteByte(byte(length)) @@ -39,6 +44,7 @@ func encodeLength(w io.ByteWriter, length int) error { } // encodedLengthSize gives the number of octets used for encoding the length. +// Reference: ISO/IEC 8825-1: 10.1 func encodedLengthSize(length int) int { if length < 0x80 { return 1 diff --git a/internal/encoding/asn1/common_test.go b/internal/encoding/asn1/common_test.go new file mode 100644 index 00000000..1dd781ca --- /dev/null +++ b/internal/encoding/asn1/common_test.go @@ -0,0 +1,86 @@ +package asn1 + +import ( + "bytes" + "testing" +) + +func TestEncodeLength(t *testing.T) { + tests := []struct { + name string + length int + want []byte + wantErr bool + }{ + { + name: "Length less than 128", + length: 127, + want: []byte{127}, + wantErr: false, + }, + { + name: "Length equal to 128", + length: 128, + want: []byte{0x81, 128}, + wantErr: false, + }, + { + name: "Length greater than 128", + length: 300, + want: []byte{0x82, 0x01, 0x2C}, + wantErr: false, + }, + { + name: "Negative length", + length: -1, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := &bytes.Buffer{} + err := encodeLength(buf, tt.length) + if (err != nil) != tt.wantErr { + t.Errorf("encodeLength() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got := buf.Bytes(); !bytes.Equal(got, tt.want) { + t.Errorf("encodeLength() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEncodedLengthSize(t *testing.T) { + tests := []struct { + name string + length int + want int + }{ + { + name: "Length less than 128", + length: 127, + want: 1, + }, + { + name: "Length equal to 128", + length: 128, + want: 2, + }, + { + name: "Length greater than 128", + length: 300, + want: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := encodedLengthSize(tt.length); got != tt.want { + t.Errorf("encodedLengthSize() = %v, want %v", got, tt.want) + } + }) + } +} From feb151cf3382f9f6e8dc7de2067cd3e454da90ce Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 18 Apr 2024 16:57:43 +0800 Subject: [PATCH 003/110] feat: crl support Signed-off-by: Junjie Gao --- revocation/crl/cache.go | 84 ++++++++++++++ revocation/crl/client.go | 75 ++++++++++++ revocation/crl/crl.go | 90 +++++++++++++++ revocation/crl/crl_test.go | 108 ++++++++++++++++++ ...81926f77973c06922d71f833ead6bef33c396db644 | Bin 0 -> 814 bytes ...b898c09e6820b06260002b8deafaef94f9a4f79ff4 | Bin 0 -> 717 bytes ...13813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b | Bin 0 -> 607 bytes revocation/crl/testdata/ms/msintermediate.cer | Bin 0 -> 1909 bytes revocation/crl/testdata/ms/msleaf.cer | Bin 0 -> 1828 bytes revocation/crl/testdata/revoked/revoked.cer | Bin 0 -> 1197 bytes revocation/crl/testdata/revoked/root.cer | Bin 0 -> 1078 bytes revocation/crl/testdata/valid/root.cer | Bin 0 -> 1391 bytes revocation/crl/testdata/valid/valid.cer | Bin 0 -> 1306 bytes revocation/ocsp/ocsp.go | 13 ++- revocation/ocsp/ocsp_test.go | 8 +- revocation/result/results.go | 2 + revocation/revocation.go | 81 +++++++++++-- 17 files changed, 443 insertions(+), 18 deletions(-) create mode 100644 revocation/crl/cache.go create mode 100644 revocation/crl/client.go create mode 100644 revocation/crl/crl.go create mode 100644 revocation/crl/crl_test.go create mode 100644 revocation/crl/testdata/cache/18d02b1ec810770d6310fa81926f77973c06922d71f833ead6bef33c396db644 create mode 100644 revocation/crl/testdata/cache/69b1d41239160438fedb94b898c09e6820b06260002b8deafaef94f9a4f79ff4 create mode 100644 revocation/crl/testdata/cache/fdd0c186cdf0cf45ef531213813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b create mode 100644 revocation/crl/testdata/ms/msintermediate.cer create mode 100644 revocation/crl/testdata/ms/msleaf.cer create mode 100644 revocation/crl/testdata/revoked/revoked.cer create mode 100644 revocation/crl/testdata/revoked/root.cer create mode 100644 revocation/crl/testdata/valid/root.cer create mode 100644 revocation/crl/testdata/valid/valid.cer diff --git a/revocation/crl/cache.go b/revocation/crl/cache.go new file mode 100644 index 00000000..3789c25d --- /dev/null +++ b/revocation/crl/cache.go @@ -0,0 +1,84 @@ +package crl + +import ( + "crypto/sha256" + "encoding/hex" + "os" + "path/filepath" + "sync" +) + +type Cache interface { + // Get retrieves the CRL from the store + Get(key string) ([]byte, error) + + // Set stores the CRL in the store + Set(key string, value []byte) error +} + +type fileSystemCache struct { + dir string + + // mu protects the cache + mu sync.RWMutex +} + +// NewFileSystemCache creates a new file system store +func NewFileSystemCache(dir string) Cache { + return &fileSystemCache{ + dir: dir, + mu: sync.RWMutex{}, + } +} + +func (f *fileSystemCache) Get(key string) ([]byte, error) { + f.mu.RLock() + defer f.mu.RUnlock() + + fileName := hashURL(key) + return os.ReadFile(filepath.Join(f.dir, fileName)) +} + +// Set stores the CRL in the store. It hashes the URL to determine the +// filename to store the CRL in. +func (f *fileSystemCache) Set(key string, value []byte) error { + f.mu.Lock() + defer f.mu.Unlock() + + fileName := hashURL(key) + return os.WriteFile(filepath.Join(f.dir, fileName), value, 0644) +} + +type memeoryCache struct { + cache map[string][]byte + + // mu protects the cache + mu sync.RWMutex +} + +// NewMemoryCache creates a new memory store +func NewMemoryCache() Cache { + return &memeoryCache{ + cache: make(map[string][]byte), + mu: sync.RWMutex{}, + } +} + +func (m *memeoryCache) Get(key string) ([]byte, error) { + m.mu.RLock() + defer m.mu.RUnlock() + return m.cache[key], nil +} + +func (m *memeoryCache) Set(key string, value []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + m.cache[key] = value + return nil +} + +// hashURL hashes the URL with SHA256 and returns the hex-encoded result +func hashURL(url string) string { + hash := sha256.Sum256([]byte(url)) + return hex.EncodeToString(hash[:]) +} diff --git a/revocation/crl/client.go b/revocation/crl/client.go new file mode 100644 index 00000000..b9264205 --- /dev/null +++ b/revocation/crl/client.go @@ -0,0 +1,75 @@ +package crl + +import ( + "crypto/x509" + "io" + "net/http" + "os" + "time" +) + +// CRLClient is an interface to fetch CRL +type CRLClient interface { + // Fetch retrieves the CRL with the given URL from remote or local cache + Fetch(url string) (*x509.RevocationList, error) +} + +type crlClient struct { + cache Cache + httpClient *http.Client +} + +// NewCRLClient creates a new CRL server +func NewCRLClient(cache Cache, httpClient *http.Client) CRLClient { + return &crlClient{ + cache: cache, + httpClient: httpClient, + } +} + +func (c *crlClient) Fetch(url string) (*x509.RevocationList, error) { + // try to get from cache + data, err := c.cache.Get(url) + if err != nil { + if os.IsNotExist(err) { + // fallback to fetch from remote + return c.update(url) + } + + return nil, err + } + + crl, err := x509.ParseRevocationList(data) + if err != nil { + return nil, err + } + + if crl.NextUpdate.Before(time.Now()) { + // cache is expired, update + return c.update(url) + } + + return crl, nil +} + +func (c *crlClient) update(url string) (*x509.RevocationList, error) { + // fetch from remote + resp, err := c.httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Read the CRL file + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // store in cache + if err := c.cache.Set(url, data); err != nil { + return nil, err + } + + return x509.ParseRevocationList(data) +} diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go new file mode 100644 index 00000000..2c8e0680 --- /dev/null +++ b/revocation/crl/crl.go @@ -0,0 +1,90 @@ +package crl + +import ( + "crypto/x509" + "errors" + "fmt" + "net/http" + "time" + + "github.com/notaryproject/notation-core-go/revocation/result" +) + +// Options specifies values that are needed to check OCSP revocation +type Options struct { + CertChain []*x509.Certificate + HTTPClient *http.Client + Cache Cache +} + +func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { + if opts.Cache == nil { + return &result.CertRevocationResult{Err: errors.New("cache is required")} + } + if opts.HTTPClient == nil { + opts.HTTPClient = http.DefaultClient + } + if !HasCRL(cert) { + return &result.CertRevocationResult{Err: errors.New("certificate does not support CRL")} + } + + crlClient := NewCRLClient(opts.Cache, opts.HTTPClient) + + // Check CRL + var lastError error + for _, crlURL := range cert.CRLDistributionPoints { + crl, err := crlClient.Fetch(crlURL) + if err != nil { + lastError = err + continue + } + + err = validateCRL(crl, issuer) + if err != nil { + lastError = err + continue + } + + // check revocation + for _, revokedCert := range crl.RevokedCertificateEntries { + if revokedCert.SerialNumber.Cmp(cert.SerialNumber) == 0 { + return &result.CertRevocationResult{ + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{{ + Result: result.ResultNonRevokable, + Error: nil, + }}, + } + } + } + + return &result.CertRevocationResult{ + Result: result.ResultOK, + ServerResults: []*result.ServerResult{{ + Result: result.ResultOK, + Error: nil, + }}, + } + } + + return &result.CertRevocationResult{Err: lastError} +} + +func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { + // check crl expiration + if time.Now().After(crl.NextUpdate) { + return errors.New("CRL is expired") + } + + // check signature + if err := crl.CheckSignatureFrom(issuer); err != nil { + return fmt.Errorf("CRL signature verification failed: %v", err) + } + + return nil +} + +// HasCRL checks if the certificate supports CRL. +func HasCRL(cert *x509.Certificate) bool { + return len(cert.CRLDistributionPoints) > 0 +} diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go new file mode 100644 index 00000000..6fa68315 --- /dev/null +++ b/revocation/crl/crl_test.go @@ -0,0 +1,108 @@ +package crl + +import ( + "crypto/x509" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/notaryproject/notation-core-go/revocation/result" +) + +func TestValidCert(t *testing.T) { + // read intermediate cert file + intermediateCert, err := loadCertFile(filepath.Join("testdata", "valid", "valid.cer")) + if err != nil { + t.Fatal(err) + } + + rootCert, err := loadCertFile(filepath.Join("testdata", "valid", "root.cer")) + if err != nil { + t.Fatal(err) + } + + certChain := []*x509.Certificate{intermediateCert, rootCert} + opts := Options{ + CertChain: certChain, + HTTPClient: http.DefaultClient, + Cache: NewFileSystemCache(filepath.Join("testdata", "cache")), + } + + r := CertCheckStatus(intermediateCert, rootCert, opts) + if r.Err != nil { + t.Fatal(err) + } + if r.Result != result.ResultOK { + t.Fatal("unexpected result") + } +} + +func TestRevoked(t *testing.T) { + // read intermediate cert file + intermediateCert, err := loadCertFile(filepath.Join("testdata", "revoked", "revoked.cer")) + if err != nil { + t.Fatal(err) + } + + rootCert, err := loadCertFile(filepath.Join("testdata", "revoked", "root.cer")) + if err != nil { + t.Fatal(err) + } + + certChain := []*x509.Certificate{intermediateCert, rootCert} + opts := Options{ + CertChain: certChain, + HTTPClient: http.DefaultClient, + Cache: NewFileSystemCache(filepath.Join("testdata", "cache")), + } + + r := CertCheckStatus(intermediateCert, rootCert, opts) + if r.Err != nil { + t.Fatal(err) + } + if r.Result != result.ResultRevoked { + t.Fatal("unexpected result") + } +} + +func TestMSCert(t *testing.T) { + // read intermediate cert file + intermediateCert, err := loadCertFile(filepath.Join("testdata", "ms", "msleaf.cer")) + if err != nil { + t.Fatal(err) + } + + rootCert, err := loadCertFile(filepath.Join("testdata", "ms", "msintermediate.cer")) + if err != nil { + t.Fatal(err) + } + + certChain := []*x509.Certificate{intermediateCert, rootCert} + opts := Options{ + CertChain: certChain, + HTTPClient: http.DefaultClient, + Cache: NewFileSystemCache(filepath.Join("testdata", "cache")), + } + + r := CertCheckStatus(intermediateCert, rootCert, opts) + if r.Err != nil { + t.Fatal(err) + } + if r.Result != result.ResultOK { + t.Fatal("unexpected result") + } +} + +func loadCertFile(certPath string) (*x509.Certificate, error) { + intermediateCert, err := os.ReadFile(certPath) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(intermediateCert) + if err != nil { + return nil, err + } + return cert, nil +} diff --git a/revocation/crl/testdata/cache/18d02b1ec810770d6310fa81926f77973c06922d71f833ead6bef33c396db644 b/revocation/crl/testdata/cache/18d02b1ec810770d6310fa81926f77973c06922d71f833ead6bef33c396db644 new file mode 100644 index 0000000000000000000000000000000000000000..bfde1f141cdfaa278498d5356d7d59b7cdfcfb7a GIT binary patch literal 814 zcmXqLV%9QfViaOxWHjJqm}?5; zSpW&Ms0B#|2?p{&b7YlSBn-qFMCLCSjb(^Sif#V#^it|_>0ndImqi9bY@FI`j4X^z z=Aw)&tW3;|42%ZcAWb4HEKE!clYlOTD_~*ahuQ(O!~|pqT5vEoF)=c<7H{`6r)cjO(zLWX?Ktq)vMi^V6df!#=iaJ)V6?%Go;J|2*rbM^o&y zJF2BT0vy*WuT{`$n=&U-@tT~P- z{v=HFna})>e{a+WpKQ9iw%_;I&10J*z1H6jIr%zUanE|reBH$7$4k7|MQAC{`>2!B zlKy7O$CLV&KgH)A+92D|_-Q5Qs;`qjSmk+_1p4keQ(12A=6qN@ef!6KTQb=H8TPmF zAJnQBIO}M{wA**vQm(|;TOPD7ReE>jPk-%-^CcTM-xuCr)U!{VgQp~M>I9C!sO=>( zI#oiAQ+7#8o2X3XHqu|)B=t!mwsDeYgY>3%%q2m3OS$D&ee8VRHRZ9w!iJ*hMOrH- z^2i)!+Pr3oT=GY8eIGl<-LG}{oezZ72z}X_ZaQ`9-fMn~y*JIzn19Gk@&CT_{QGVl QUjA~|zOD67F0B;=06lL$EdT%j literal 0 HcmV?d00001 diff --git a/revocation/crl/testdata/cache/69b1d41239160438fedb94b898c09e6820b06260002b8deafaef94f9a4f79ff4 b/revocation/crl/testdata/cache/69b1d41239160438fedb94b898c09e6820b06260002b8deafaef94f9a4f79ff4 new file mode 100644 index 0000000000000000000000000000000000000000..f7df094da08897c1942d7ad9a608b81c8d544867 GIT binary patch literal 717 zcmV;;0y6zDf&$4ffwBSt0Wb{)2`Yw2hW8Bt0Sg5HFi$ZHFbM_&RRjhT0##EnDKIAn z162eH6Cg=$bY*gGWpp4@Wn*=6X>@rYQe|^xVRB<=AV+dGoDJ>9*D<6j{P|gxv#s&T;zB>NAtcGc*G-P!D{J_Wiguo zBoC1wm>)+~%ByN}t*p7g`8-b>Mp)QTkLMG6|NCEy!MXvLH+M83sMSrdobr!tBC_3I z1-2~SnWDDp?^$Hk!LWaTMa*c2d}Hfl2k}W7k2vH7EqvfYn63v0i}##6sR?kuk5bbx z>fb_Vh-=c`t34>Bde?osf^TXNmH|pXf!E>VT9H_uE%NzZSkKO5qrFCt43}(Am(@t# zoXc0kn#Q7%c;*E?)lL-I`1A>$(*6@TTj{R~?2_ld?piZQ?lFSbZwt-rs@rCt*r8(} zS9^A<(AK3M$wWonJQ%%i1-Kblpy|bmSa3V!BEJpDqu^7fe=DXr6s;=_w@OCRcp+zN zB{L(|s~3Q~W+LqFZL#G6u1;HAJ2g~3T9i@sf$Pf<%GNo47q0M4C4pqkwgw66b?XiW z<)pwK4kVv5zx^K)NLOItr0A-$FzM{zQg%d6y+Ubewcb(hgrpW3Nun##Jz}(O(7iO0 literal 0 HcmV?d00001 diff --git a/revocation/crl/testdata/cache/fdd0c186cdf0cf45ef531213813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b b/revocation/crl/testdata/cache/fdd0c186cdf0cf45ef531213813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b new file mode 100644 index 0000000000000000000000000000000000000000..56929dee5d453cc1c727a1126d8ea0daaea5b657 GIT binary patch literal 607 zcmXqLVv06sVsvI=WHjJq~179`@kGoV5I+6hkQk36KaEkDzmYZhlI>g0rK7 zPiAgrNotCrqJca}j+sZo(a}-CIkl)HGc7Y2Xr@ANYEfBca%!q|yPLiEdae>X*1)}& zGHcc{$=WPBY7iW|eZ#|!q&qHG{tFuB_fI{uSR?HAWz#nw)O?EGi==JS^xCrg%FpYb zeZ1YqzZ(}CPUL?Xu;QTWuh-t?o0X>3*4{tJD`FBDcId*&e!J&99Cu98-Yj3rw*HP; zr@d`XN2gnC`Rwc`_Awn#Wi9y6$nFmEjri}EW#QbOcJpJ1_vDg3*=rGV+yBH~x%jq2 zdPmyw)k2oI%!<}!wY mT}_dXQm;Sl7n6Q&)GoL{=d|*+*I9?dy)Sa=TCCwr+6@4HZPtST literal 0 HcmV?d00001 diff --git a/revocation/crl/testdata/ms/msintermediate.cer b/revocation/crl/testdata/ms/msintermediate.cer new file mode 100644 index 0000000000000000000000000000000000000000..029cdd444e45b8684c9852067ddec1a8a9e59048 GIT binary patch literal 1909 zcmcIkX;f3!8qK{KfB+H+NU#P`qk;lCNhE+85M>YyQ^L?PX-F6nNN%{fQ51+8l$N$Y zc~YNJMI0&46Ht*s5R}5Ii0>&NP?7Qkd`fN6C|0mH&-(0IZ>|1&zs}ivf9E@Uee0YJ z^d^}=PyVEW2tzPT=Me}5+qTj4a`>B|{rDb7z>;FNEPs9eOG^kcClkQV&p775j6#ee zQRr}#m_rA%jlh&njge^5E*QE_NsmXS1}n~0KSN<<~&IDmOZhDx^$kxNlTgQQ@N z>kw3ppb|`upWp&u)QNQG54s3J@SmF-o$ph~o&j>-z|HbJNzi;o4alpjnPE|&zw!OnW#sF>42p$@4 zxZ-WMiT5@>>}JZ#uf>_M!@A(uneX_iy8{+kR2R~_ws5b}7-O4q6{0-9+7m?q=_WRl zZJS@@hYvLSwP(gha6Brgo~pv9JyyL-#5rCPcw6hqX;glWdFr%*-{`(Xc7NZhy1CiB zGdtpRU()6s6wTC(B`qFMSal^lpD7Y|-Fvb-I^^fU`od#Bl@wp}a)~%Fu9jPN@LknK zoOXAohMz8f%DO)EeX*`OvUuP%IclFWZBf*9?)-P2|7d`!gM0T{K8O!< z8BV(R!(q7rEdE1)!#}Sn1X4?E={g)S%CJ3){QF$tqbl=A9u{v zz3D6We^7(6`*Sz$sOvwe<3HgG4a-&ET1SjEk%>pUH77zmu+n)e^j6Pkzt#pao>q?S z&+!rVpG$0+yxXf0tP5@Cc?pZ)`n73pw8y(50*1RA6pUC2r0GaKlGx+486M^_(4I=i zXDzR-sXbD};(NaAi@1stc?%LY0<5I8wD(H*lzD+rLr5oZ! z5ee4)i|fvXlecBG&1RxM7fy*h%5R*_ zuC4V}47;6yi4Xxgs)q)F9_j|9Y#|c)KN?u{frCkNNH)tVMKiM2vPON$ z>Hv4=F4|i*X#z0u$H<&zln+(fCJ+*n67rt^>rQgzM`CyTaTLTGWk*sJ5Q7Mc#UED) z^Ku~w_yQk_DbA3|CNMOImxf{Lm2CEwEnD1_@4u*<6j8F(>2gG^Vf!Ic4O^6~Ml^B^ zLASc4VM;(Zc3(`g0Mz%Lh^D~)zqV~)l7$16ASP%DNrZI-Utn&mw<3}7w8;lnMnH$4 zwBo>hvV9pMkyN2k_;;|#~Non1J(y!urZj~`wLA0@8J}C@@fDIKv6@mHw zf0&k{8UJgVjk9buipaE5OvA=ME1oBKn&2U_(HI}DfbYuXx#2}1_Pu)uB837W2!4+N zobUZtb74#Ibo#5_*qo^T@isNyp@Fij z3zR*77jSshUOg?De4qQWjGWtAFXdG$&PYq~ZyDW(OBo9%##2&PibDqX)zvs;_43^g zROzD$#Pj6Ff-(6}o!Lb80|nCP+^OpIS+9lq7(XiY7B7^GB1f{sT2{=J!9$Zpf*X@* z50VYXqEEj3y2dGL>1|fSnu;d-8f)v>34*0VYxa7cemUh_aJ}B{eB|!6m)=;Ee_>xR z`pjIhvt_6KT6>Z9bAAi+xjPH92giu2og!)K@;53X5#Gg5g_imi9K5E|&(Y(vUy zl1;H1HOh`6vxZAhOi-k(Pt&vAA)&rX&}|!iB;#hp-lDLg2;4*|Lnl3 zUuw^`j};__9lUc}xVbte((Hm9^3&xUmSkvVs8^r(rU>NIoz1>MI^CYe&R45OYHgb? z-|{}riP%-&1UKuCWZN-q3kuaUqt&;a<`$S2^Y7OQr@nbh9bfT^<(|5H$nv{KU+%y6 zaN*-;OVX-IYD?eXpnzX%7Eft3pPUw}# W$+YH7ih_2T8O8hf(qVPVufGEcve|I} literal 0 HcmV?d00001 diff --git a/revocation/crl/testdata/ms/msleaf.cer b/revocation/crl/testdata/ms/msleaf.cer new file mode 100644 index 0000000000000000000000000000000000000000..1789f2293fbc3fa85acda51dd9c7b990a524d4d0 GIT binary patch literal 1828 zcmcIkc{tQ*9RK}h#>|)@$B3Lwj*#meOzv9e*xee#5FIKr#u}N!%tVO{F)fv+bda*y z4m`4z4qe(r$hnbJ%C#6e6bfa+&TO^y-|j#A&-*-|&*%GokN5e0UXX?%f;1&;Arb*2 z5CjJZg6}6_OW&N*sq2Pd1p=yQ^aphsTX1836+YgCi_l&1MRCLSB@J=*<)Gd4dQLTYh6H zFjeSCg5g&^n9X6?&_xj(K9T0_Nwfzf0E~20?HvHwo=kEf*^?aC$)O{VE1w}qZTer@ zGXo}y_V5IQukCqqSOPXPf*8POeHubYG6!a#!i>1|Xf`W0k{C><6MgAaS5E+d4Iujh z8y^=(C(`G9$Zzv~GcKJah-WidAPr&s?eoJ*5Hby+2FZCGA`JmUz4oEQy}553e5(R2 zf0mX=+ExYZ;ZhD*r|xobYCW=`H$1$fAEo1=ei~8H(-LmFZUB>q{9poxSESddU&CGL zcvrN)pGhf6>|ZNmr|oOrpIbgTBXxUVwv{qK<0f9nxDgSU$RN*6U7j>`@=}Q(k;XG( zYK+B<{Hmxaqe9u7e;ego+pVx2-kDvOmAC^%n+B`jWEzs3a#i&?q$&eqMQ5HSHT1Ao z7iuT9AZcoQ47Psol*;QnSNC(Fg?ZyvQeVuS29M)K2OOwwDLY$8#p11e_@SDz-<@OU zCv~3nB!t(&TP)O6!r~ir0`k}TE+T3r>DUxr3!xMyWVgJ&S6+GEucCZ@-YGS!WW06l z{HE8h{hQ*PjflO=nHRRN?qX|(Vfo;@f)DOY${*Q}_)pk9*&8oe@5tMPLB@<` zcR1LdG%p~BJ z7l2KQfW@dVum%Jd1{KmI)M$~2?`mh4kdR=@`C?mJCXZvs-@@kcg?3Cqtld9`FtZ0= zS6B|@o|T^p*IJM)ZRMdL7LBz+!%9jR7)EUd8^MMz7$79Hf33w5h=l*Wnh2;U#v#D) zJPH9Ipshe=3Zw#XU+N;Ue}7blY6>2OL}0|<+D-o3$?XP1xx0r=aenKN)EqLew65cp zSY$b1R5ycRc>iQJGS4V$uxxx5Cw#-nqcB+HU=FLP~=iDzKNKFz2sh^afY zTl1A9*kY=*lkAw^d`8wtZ144&Ex7UFY+R3QiBUQ2x&>GjA5!clJ6M%>T-&1dv43a( zuO&N^r7?Sce_(yrJEgPHjV_(0pR7J_%dNpU43B(t^*{$EKb={+`90pT)*&&#M{kU< zQ8+OsT$bnPxAy?&FjOj)Ozs}*+qJs3&-uwPq=iwMY@D5_ijDL8F{H({Yv$?ud;<+} z&IL(sA!BTM>{9V7nOYg4YdRw(WS76SPb0=}KreHLm0m>I^~|Odd_cwTjd|ulsdcG5 zw&sWRSFrT!E4}XCLK%|^;>%HG-zlrAx_SKO{BXJRy&#=>(&1{^qsm$uE!7lLen?{=F+=Ju7L2yrKkT)x$%Nb~joUXzqv7?*U=V@TuKF)jw9Tgg literal 0 HcmV?d00001 diff --git a/revocation/crl/testdata/revoked/revoked.cer b/revocation/crl/testdata/revoked/revoked.cer new file mode 100644 index 0000000000000000000000000000000000000000..781deca25aab3239c60c678ee714474fb634d13d GIT binary patch literal 1197 zcmXqLVp(a>#5{2UGZP~dlR!;P?B2gkf>Oo%&i+wn-0{JHmyJ`a&7D}zC` zA-4f18*?ZNn=q5RlcBVMB#6VoBjjF`npl!rq~M#Fmz+nI)+yhKdIAAUS3p2}egq1?SYFlFYQsWT2S} z!Kp=MnaQce26E!O1||lEKnMm=;=D!{hDOFfWNK+@8D-G84#{<)!7vVoFjHuVp}2u4 z*fqj@K-ZKg_#|ehDmZ7BR2s?|$bcOsEE=P4xSr6{=OCT8XsG%+e62QVWm19KB2KLb#li>Zl`kztc%>yl5ks~xSA z+|1XVJ(y6s$1?S&mz4XWoo77GWX-thYbv>OiPO__o{zfpggZ8;Tk2EO#T~ed-pBLK%__K zH4`%<1LNYxF9wYt!Kp)5nMJ}ttU+XfAeXY;w2$m>9v%{|+7EP1uyv?$BZ$oQXy1(?*?4ERCHg+Y8)17;v)AP5pr zVBs?0FkoY20Wuj3TtGtdEVc&L29^uV7nrsgLz6&qQ4VSn&<7@3q%;dlvU)&;2BvHr z+HAnw!_LUaqG_ORpbFy~Ft$lT4a`q2E z%2{1m-j{>;Ue4Ql&_c@aez!@=4vY4M3#YCK{>u}!qxH_ivaQ>CJD*%t*KDZrHe9gU z!9=*+)ao-QN72>1C^x406-&}jFrPo%TXH;lckS$4_Y140+*`>bX!4L#s_6NN&?%Rn zPZsiRaoi)eFIaOcv+Zy93u?ztd~e?3x8a6bM5D1=_!Nr>=InjzwuPJ!ko%$a_|IMQ zSC;)xkAD+SQagKjzSP5|3KQ>#9J_bvQ%-42DYq^ECYx?Ik&274=WghF#PDz^OKvGj zF7V82d;L`MwRakpdu7GdNaclPa&=*(sn?0R=`onMMjRG=L)wC`G0J2iD A&j0`b literal 0 HcmV?d00001 diff --git a/revocation/crl/testdata/revoked/root.cer b/revocation/crl/testdata/revoked/root.cer new file mode 100644 index 0000000000000000000000000000000000000000..5c715faed85eae3569262d7f3dd3fa93a62dff60 GIT binary patch literal 1078 zcmXqLVlgskVwPIK%*4pV#K>sC%f_kI=F#?@mywZ`mBFCeklTQhjX9KsO_<5u$xzxr z62#%)5ppj|O)N<*Qt(a8OU_6w1~Lr=4ER8j>^$tji8*QcMJa|-1`;3BO}8;hq+(!9=D%XaqM2>(L9Krh@_kR3etljY8_*?Y$UTeg!qEbyv!1#k$!E6ZWw>z%zdG%s za$rx9->Z+J>%N4{65={swaGZ)(XGzV&q@)W@7+27zPW8t&lLap8J}iMaBERNUbg@7 z8B?9^IQEp(rUM=uR!sbvIKNCGV8z7aXy`MkR z^P}$lH6^~%Gq$=PeX4o!=LCu4Npto!A6Kik>ry>_;O^XKrWpbXbNg@HyL7l^4sYBG z{gX`0j0}v68xI&X?lq7F#dY#EPc0sPf3X5{=a{Ob{zMQ z`qG)eTA#3;)$8-OkB9aj*;&%IO}94t%%#t6clD1w$U1!3gjuI{+TEx_e9O6J9%Sua z&7=Q0#_D{Ic_?T5yAv>AB-zo?y~ literal 0 HcmV?d00001 diff --git a/revocation/crl/testdata/valid/root.cer b/revocation/crl/testdata/valid/root.cer new file mode 100644 index 0000000000000000000000000000000000000000..9d2132e7f1e352fabac7eafb231488b5da91ffb2 GIT binary patch literal 1391 zcmXqLV$C*aVh&!w%*4pVB*@StaDKxjhsTjF$q#lXH+3@@@Un4gwRyCC=VfH%W@Rw& zH{>?pWMd9xVH0Kw4K~y?PzQ0igcUsVN>YpRQcDzqQMi96N{2F6x@sQ zOA8D|4TM2TnT2^ggM-`^g7WiA6e0`_)UeSUJ_S;M+V-u>HW)=goaf7yL{%}fvF;1?F_{JHX*^)7mb z_cWAjyQP1@qPLp4KvBB%lYz~z{&jb6C9i%h=6|S9(7WzD_ly5q%k{o&s`h%|Bc#ex z(95j3;9;=J8{wPpB=-w!_Uf_kT$~tqZ%sS8l;RAn=gy-c5l%vESRjulRoaDHHpQelw1#&mWmj<25Ut_nWV1qwMTG%s)L@ zZ#3Rz-J*5P@#PxEvZ-ABH|}5EDDklY(M=kbokat@+bL(=ez`Qo=d9_8$g;*;h-`WLMh;lRc_g>Iv-DFqo zCF5PpD)i^rs|NwXHO`YuHlHea-Y3t;=GdnK4#`;nE(6$dNYTB&bR(NQ2+$oz?wqHJLsjX!HYm3h*_fBZ@a%uek ze*2NA(-ox)>ah}I#svAgPldH?sMd^L9VXJTe#U|j5E;9$T9Os}&1 zjEw(TSb({M&43@o7Y6ZJ4VZzHfhFz=l^iUlGsD^9O_ z?o;@C)1`#9mMgeli7SS+ehlD?e0}ag-X~KPhVT7{&D4o6YKug*3J5*#Pa(8&H7gpwUsuC^Ywq~GKr43@rUtb$j%*V zXSzC!JAHIpY?|)Bn-;WsJ~s2)HigcR z-KW{sqcnToqipMNtEK|qJDkTmPjj*R=DdjQJNf?H>f^h&YWulf^SYpR=4sI>j;y6q zAB!&hzU1vmo%p4{|F6+t(%W~vdiUeP>Iq_(+2h=TYs}f5dM+QCHs|Whty&MJN;P<_ z^RZ+cWl3o${YKsGG(t*4WP8`@Gk9!gJ;MzXRq} z=D1zmBD#56Ufpb-X;wRebnUN2Km5&csO6u^ip8C`)?_`D(Av1dIWhXO{2lAwvQN4% zdQ0z%8|T;t|E@mm82|syq6>)@52x)|6WeWmz4WT_ftiBq<~klMDs9=v&JYE{~5g;AP{~YV&CO&dbQi&B|cl zZ^&)H$;KSY!Y0fV8f>U(pbp}22`hN!m82HsrIsiJrzV#cWtLPb1f>?ICKe@UD7Y8p zmlha`8VG}wG7Ix~1_!w-1m)+KC`1?<$cghB85md^m>7UT6p(9bU}i>{#klr@pXRpDI%e!;XU(K zEV7jR+GOLj(l76;^&6z(8 z#AA0A<^OIxy7p3Agsu4T=bXDgYJYFLbMRJS>=n1iXV$77?AVi#UYfS~qr~?`G0%ek zTXk%6U;1BI;?)e!a{IZ#KhHBh{kp6`Tx5OnlK(R|Po8@xcsbiYk5<`*nd?+bcMG2h zV*dIzaAEgtQ6^?a2FArrj2yraVKLwX2B<7QBjbM-7GNT1Gmr)GRarnG&7sZ4$jZvj z%mimK8VG@;g+aGrR#$eo~%5Jf`OcY%mS$e637aZkrgU|Y*%2BHjp&nU}H;f0^%a4a^@zWr&>?>x!W!N-s;l2=W2SzrWwg=OMT_0*&3%7h3Gae zcy;*g4~6~lXSNqGY|pd)7B}VI6NUN9-gj?ee!gg{n9amW8&X z%-(q}=2Oav1N$lu`1j?y@Wf5pt@piK;Nc5d7tPy|3U8BlD*g~sn=(0kfov+vK`y0r z0=&-C7fQ(2J$TS&zBOv&UW5JZdD>e475$`H4}X=I{vmG7;iWsWKIrYSHs1Np``(#9 zPu90^x7i;EbvFB!@z6{>t8eDT|4SW~n`}RDA=%Q@vNZ40uCf!8nO?5+&JjPy!*p|R z$I5B||t$-I`!kiAsP`9|pUq9dAo-;c!loml7AVsQOaYrMq5%H7Z7 z3cA@JwoN{~v;R(Fp{myU`)^ePf-<@%-FbR#>*HIs7us`L6b;ukef_<2^@&b#+lM|+ zE%?6e)!sX;QRMa2+qMeJ>mn~d`VsLndWXl^e=+`In*ZcNmDisT+|c`~X7U7a{l9A# zak{(Ne|WiJ`+p7J45Mr5adMf9C-3+=w_Bh4Qjqhqe53GGU!%tR7QwBtb+KuhuXfyh uGIi_Otzkk=XOH+DQ?+mj$bEB;AyneuOV5-mey66-*%E!Ac*W`+?uP)U>;Po| literal 0 HcmV?d00001 diff --git a/revocation/ocsp/ocsp.go b/revocation/ocsp/ocsp.go index d3def3c0..cc26e5dc 100644 --- a/revocation/ocsp/ocsp.go +++ b/revocation/ocsp/ocsp.go @@ -77,7 +77,7 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { // Assume cert chain is accurate and next cert in chain is the issuer go func(i int, cert *x509.Certificate) { defer wg.Done() - certResults[i] = certCheckStatus(cert, opts.CertChain[i+1], opts) + certResults[i] = CertCheckStatus(cert, opts.CertChain[i+1], opts) }(i, cert) } // Last is root cert, which will never be revoked by OCSP @@ -93,15 +93,15 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { return certResults, nil } -func certCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { - ocspURLs := cert.OCSPServer - if len(ocspURLs) == 0 { +func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { + if !HasOCSP(cert) { // OCSP not enabled for this certificate. return &result.CertRevocationResult{ Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})}, } } + ocspURLs := cert.OCSPServer serverResults := make([]*result.ServerResult, len(ocspURLs)) for serverIndex, server := range ocspURLs { @@ -271,3 +271,8 @@ func serverResultsToCertRevocationResult(serverResults []*result.ServerResult) * ServerResults: serverResults, } } + +// HasOCSP returns true if the certificate supports OCSP. +func HasOCSP(cert *x509.Certificate) bool { + return len(cert.OCSPServer) > 0 +} diff --git a/revocation/ocsp/ocsp_test.go b/revocation/ocsp/ocsp_test.go index 4afca12b..3ff24c1d 100644 --- a/revocation/ocsp/ocsp_test.go +++ b/revocation/ocsp/ocsp_test.go @@ -93,7 +93,7 @@ func TestCheckStatus(t *testing.T) { HTTPClient: client, } - certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) + certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) }) @@ -105,7 +105,7 @@ func TestCheckStatus(t *testing.T) { HTTPClient: client, } - certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) + certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{{ Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ @@ -122,7 +122,7 @@ func TestCheckStatus(t *testing.T) { HTTPClient: client, } - certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) + certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{{ Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ @@ -140,7 +140,7 @@ func TestCheckStatus(t *testing.T) { HTTPClient: client, } - certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) + certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) }) diff --git a/revocation/result/results.go b/revocation/result/results.go index c7ecba51..d106cbd4 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -100,4 +100,6 @@ type CertRevocationResult struct { // Otherwise, every server specified had some error that prevented the // status from being retrieved. These are all contained here for evaluation ServerResults []*ServerResult + + Err error } diff --git a/revocation/revocation.go b/revocation/revocation.go index 287935bb..d8917b28 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -19,10 +19,13 @@ import ( "crypto/x509" "errors" "net/http" + "sync" "time" + "github.com/notaryproject/notation-core-go/revocation/crl" "github.com/notaryproject/notation-core-go/revocation/ocsp" "github.com/notaryproject/notation-core-go/revocation/result" + coreX509 "github.com/notaryproject/notation-core-go/x509" ) // Revocation is an interface that specifies methods used for revocation checking @@ -36,6 +39,9 @@ type Revocation interface { // revocation is an internal struct used for revocation checking type revocation struct { httpClient *http.Client + + // CRLCache caches the CRL files, the default one is memory cache + CRLCache crl.Cache } // New constructs a revocation object @@ -43,23 +49,78 @@ func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } + return &revocation{ httpClient: httpClient, + CRLCache: crl.NewMemoryCache(), }, nil } -// Validate checks the revocation status for a certificate chain using OCSP and -// returns an array of CertRevocationResults that contain the results and any -// errors that are encountered during the process -// -// TODO: add CRL support -// https://github.com/notaryproject/notation-core-go/issues/125 +// Validate checks the revocation status for a certificate chain using OCSP or +// CRL and returns an array of CertRevocationResults that contain the results +// and any errors that are encountered during the process func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { - return ocsp.CheckStatus(ocsp.Options{ + if len(certChain) == 0 { + return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} + } + + if err := coreX509.ValidateCodeSigningCertChain(certChain, nil); err != nil { + return nil, result.InvalidChainError{Err: err} + } + + ocspOpts := ocsp.Options{ CertChain: certChain, SigningTime: signingTime, HTTPClient: r.httpClient, - }) - // TODO: add CRL support - // https://github.com/notaryproject/notation-core-go/issues/125 + } + + crlOpts := crl.Options{ + CertChain: certChain, + HTTPClient: r.httpClient, + Cache: r.CRLCache, + } + + certResults := make([]*result.CertRevocationResult, len(certChain)) + var wg sync.WaitGroup + for i, cert := range certChain[:len(certChain)-1] { + switch { + case ocsp.HasOCSP(cert): + // do OCSP check for the certificate + wg.Add(1) + + // Assume cert chain is accurate and next cert in chain is the issuer + go func(i int, cert *x509.Certificate) { + defer wg.Done() + certResults[i] = ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) + }(i, cert) + case crl.HasCRL(cert): + // do CRL check for the certificate + wg.Add(1) + + go func(i int, cert *x509.Certificate) { + defer wg.Done() + + certResults[i] = crl.CertCheckStatus(cert, certChain[i+1], crlOpts) + }(i, cert) + default: + certResults[i] = &result.CertRevocationResult{ + Result: result.ResultNonRevokable, + ServerResults: []*result.ServerResult{{ + Result: result.ResultNonRevokable, + Error: nil, + }}, + } + } + } + + // Last is root cert, which will never be revoked by OCSP + certResults[len(certChain)-1] = &result.CertRevocationResult{ + Result: result.ResultNonRevokable, + ServerResults: []*result.ServerResult{{ + Result: result.ResultNonRevokable, + Error: nil, + }}, + } + wg.Wait() + return certResults, nil } From 56da59c01948a5c8593585f0ac680ce4d9842fe5 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 18 Apr 2024 16:59:22 +0800 Subject: [PATCH 004/110] fix: update Signed-off-by: Junjie Gao --- internal/encoding/asn1/asn1.go | 287 ----------------------- internal/encoding/asn1/asn1_test.go | 314 -------------------------- internal/encoding/asn1/common.go | 58 ----- internal/encoding/asn1/common_test.go | 86 ------- internal/encoding/asn1/constructed.go | 43 ---- internal/encoding/asn1/primitive.go | 41 ---- 6 files changed, 829 deletions(-) delete mode 100644 internal/encoding/asn1/asn1.go delete mode 100644 internal/encoding/asn1/asn1_test.go delete mode 100644 internal/encoding/asn1/common.go delete mode 100644 internal/encoding/asn1/common_test.go delete mode 100644 internal/encoding/asn1/constructed.go delete mode 100644 internal/encoding/asn1/primitive.go diff --git a/internal/encoding/asn1/asn1.go b/internal/encoding/asn1/asn1.go deleted file mode 100644 index 8e58294e..00000000 --- a/internal/encoding/asn1/asn1.go +++ /dev/null @@ -1,287 +0,0 @@ -// 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. -// - Indefinite length is not supported. -// - The length of the encoded data must fit the memory space of the int type (4 bytes). -// -// Reference: -// - http://luca.ntop.org/Teaching/Appunti/asn1.html -// - ISO/IEC 8825-1 -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. -// To get the DER of `r`, encode the values -// in the returned slice in order. -// -// Parameters: -// r - The input byte slice. -// -// Return: -// []value - The returned value, which is the flat slice of ASN.1 values, -// contains the nodes from a depth-first traversal. -// error - An error that can occur during the decoding process. -// -// Reference: ISO/IEC 8825-1: 8.1.1.3 -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. -// -// Parameters: -// r - The input byte slice. -// -// Return: -// []byte - The identifier octets. -// []byte - The content octets. -// []byte - The subsequent octets after the value. -// error - An error that can occur during the decoding process. -// -// Reference: ISO/IEC 8825-1: 8.1.1.3 -func decodeMetadata(r []byte) ([]byte, []byte, []byte, error) { - // structure of an encoding (primitive or constructed) - // +----------------+----------------+----------------+ - // | identifier | length | content | - // +----------------+----------------+----------------+ - 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. -// -// Parameters: -// r - The input byte slice from which the identifier octets are to be decoded. -// -// Returns: -// []byte - The identifier octets decoded from the input byte slice. -// []byte - The remaining part of the input byte slice after the identifier octets. -// error - An error that can occur during the decoding process. -// -// Reference: ISO/IEC 8825-1: 8.1.2 -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 - // Reference: ISO/IEC 8825-1: 8.1.2.4 - 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 -// -// Parameters: -// r - The input byte slice from which the length octets are to be decoded. -// -// Returns: -// int - The length decoded from the input byte slice. -// []byte - The remaining part of the input byte slice after the length octets. -// error - An error that can occur during the decoding process. -// -// Reference: ISO/IEC 8825-1: 8.1.3 -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 - // Reference: ISO/IEC 8825-1: 8.1.3.4 - return int(b), r[offset:], nil - } else if b == 0x80 { - // Indefinite-length method is not supported. - // Reference: ISO/IEC 8825-1: 8.1.3.6.1 - return 0, nil, ErrUnsupportedIndefiniteLength - } - - // long form - // Reference: ISO/IEC 8825-1: 8.1.3.5 - n := int(b & 0x7f) - if n > 4 { - // length must fit the memory space of the int type (4 bytes). - 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. -// Reference: ISO/IEC 8825-1: 8.1.2.5 -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 deleted file mode 100644 index 3cc94795..00000000 --- a/internal/encoding/asn1/asn1_test.go +++ /dev/null @@ -1,314 +0,0 @@ -// 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 ( - "fmt" - "reflect" - "testing" -) - -func TestConvertToDER(t *testing.T) { - testData := []struct { - name string - ber []byte - der []byte - expectError bool - }{ - { - name: "Constructed value", - 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: []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2d, - - // Type identifier - 0x06, - // Type length - 0x09, - // Type content - 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, - - // Value identifier - 0x04, - // Value length in BER - 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, - }, - expectError: false, - }, - { - name: "Primitive value", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0x20, - // length - 0x81, 0x01, - // content - 0x01, - }, - der: []byte{ - // Primitive value - // identifier - 0x1f, 0x20, - // length - 0x01, - // content - 0x01, - }, - expectError: false, - }, - { - name: "Constructed value in constructed value", - ber: []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2d, - - // Constructed value identifier - 0x26, - // Type length - 0x2b, - - // Value identifier - 0x04, - // Value length in BER - 0x81, 0x28, - // Value content - 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, - 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: []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2c, - - // Constructed value identifier - 0x26, - // Type length - 0x2a, - - // Value identifier - 0x04, - // Value length in BER - 0x28, - // Value content - 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, - 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, - }, - expectError: false, - }, - { - name: "empty", - ber: []byte{}, - der: []byte{}, - expectError: true, - }, - { - name: "identifier high tag number form", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x81, 0x01, - // content - 0x01, - }, - der: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x01, - // content - 0x01, - }, - expectError: false, - }, - { - name: "EOF for identifier high tag number form", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, - }, - der: []byte{}, - expectError: true, - }, - { - name: "EOF for length", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - }, - der: []byte{}, - expectError: true, - }, - { - name: "Unsupport indefinite-length", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x80, - }, - der: []byte{}, - expectError: true, - }, - { - name: "length greater than 4 bytes", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x85, - }, - der: []byte{}, - expectError: true, - }, - { - name: "long form length EOF ", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x84, - }, - der: []byte{}, - expectError: true, - }, - { - name: "length greater > int32", - ber: append([]byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x84, 0xFF, 0xFF, 0xFF, 0xFF, - }, make([]byte, 0xFFFFFFFF)...), - der: []byte{}, - expectError: true, - }, - { - name: "length greater than content", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x02, - }, - der: []byte{}, - expectError: true, - }, - { - name: "trailing data", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x02, - // content - 0x01, 0x02, 0x03, - }, - der: []byte{}, - expectError: true, - }, - { - name: "EOF in constructed value", - ber: []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2c, - - // Constructed value identifier - 0x26, - // Type length - 0x2b, - - // Value identifier - 0x04, - // Value length in BER - 0x81, 0x28, - // Value content - 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, - 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, - }, - expectError: true, - }, - } - - for _, tt := range testData { - der, err := ConvertToDER(tt.ber) - fmt.Printf("DER: %x\n", der) - if !tt.expectError && err != nil { - t.Errorf("ConvertToDER() error = %v, but expect no error", err) - return - } - if tt.expectError && err == nil { - t.Errorf("ConvertToDER() error = nil, but expect error") - } - - if !tt.expectError && !reflect.DeepEqual(der, tt.der) { - t.Errorf("got = %v, want %v", der, tt.der) - } - } -} diff --git a/internal/encoding/asn1/common.go b/internal/encoding/asn1/common.go deleted file mode 100644 index 7c7110e7..00000000 --- a/internal/encoding/asn1/common.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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. -// Reference: ISO/IEC 8825-1: 10.1 -func encodeLength(w io.ByteWriter, length int) error { - if length < 0 { - return ErrUnsupportedLength - } - - // 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. -// Reference: ISO/IEC 8825-1: 10.1 -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/common_test.go b/internal/encoding/asn1/common_test.go deleted file mode 100644 index 1dd781ca..00000000 --- a/internal/encoding/asn1/common_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package asn1 - -import ( - "bytes" - "testing" -) - -func TestEncodeLength(t *testing.T) { - tests := []struct { - name string - length int - want []byte - wantErr bool - }{ - { - name: "Length less than 128", - length: 127, - want: []byte{127}, - wantErr: false, - }, - { - name: "Length equal to 128", - length: 128, - want: []byte{0x81, 128}, - wantErr: false, - }, - { - name: "Length greater than 128", - length: 300, - want: []byte{0x82, 0x01, 0x2C}, - wantErr: false, - }, - { - name: "Negative length", - length: -1, - want: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf := &bytes.Buffer{} - err := encodeLength(buf, tt.length) - if (err != nil) != tt.wantErr { - t.Errorf("encodeLength() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got := buf.Bytes(); !bytes.Equal(got, tt.want) { - t.Errorf("encodeLength() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestEncodedLengthSize(t *testing.T) { - tests := []struct { - name string - length int - want int - }{ - { - name: "Length less than 128", - length: 127, - want: 1, - }, - { - name: "Length equal to 128", - length: 128, - want: 2, - }, - { - name: "Length greater than 128", - length: 300, - want: 3, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := encodedLengthSize(tt.length); got != tt.want { - t.Errorf("encodedLengthSize() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/encoding/asn1/constructed.go b/internal/encoding/asn1/constructed.go deleted file mode 100644 index 409fd5a4..00000000 --- a/internal/encoding/asn1/constructed.go +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index 0c2cdec6..00000000 --- a/internal/encoding/asn1/primitive.go +++ /dev/null @@ -1,41 +0,0 @@ -// 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 -} From a25275e04b5de61b2762930d60723612c2b79fce Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 29 May 2024 08:37:37 +0800 Subject: [PATCH 005/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 154 ++++++++++++++++++++++++++++++++--- revocation/crl/crl_test.go | 6 +- revocation/crl/error.go | 13 +++ revocation/result/results.go | 77 +++++++++++++++++- revocation/revocation.go | 2 +- 5 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 revocation/crl/error.go diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 2c8e0680..210708e3 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -1,13 +1,24 @@ package crl import ( + "bytes" "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "errors" "fmt" + "math/big" "net/http" "time" "github.com/notaryproject/notation-core-go/revocation/result" + "golang.org/x/crypto/cryptobyte" + cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" +) + +var ( + oidDeltaCRLIndicator = asn1.ObjectIdentifier{2, 5, 29, 27} + oidFreshestCRL = asn1.ObjectIdentifier{2, 5, 29, 46} ) // Options specifies values that are needed to check OCSP revocation @@ -19,13 +30,13 @@ type Options struct { func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { if opts.Cache == nil { - return &result.CertRevocationResult{Err: errors.New("cache is required")} + return &result.CertRevocationResult{Error: errors.New("cache is required")} } if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } if !HasCRL(cert) { - return &result.CertRevocationResult{Err: errors.New("certificate does not support CRL")} + return &result.CertRevocationResult{Error: errors.New("certificate does not support CRL")} } crlClient := NewCRLClient(opts.Cache, opts.HTTPClient) @@ -49,25 +60,18 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR for _, revokedCert := range crl.RevokedCertificateEntries { if revokedCert.SerialNumber.Cmp(cert.SerialNumber) == 0 { return &result.CertRevocationResult{ - Result: result.ResultRevoked, - ServerResults: []*result.ServerResult{{ - Result: result.ResultNonRevokable, - Error: nil, - }}, + Result: result.ResultRevoked, + CRLStatus: result.NewCRLStatus(revokedCert), } } } return &result.CertRevocationResult{ Result: result.ResultOK, - ServerResults: []*result.ServerResult{{ - Result: result.ResultOK, - Error: nil, - }}, } } - return &result.CertRevocationResult{Err: lastError} + return &result.CertRevocationResult{Result: result.ResultNonRevokable, Error: lastError} } func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { @@ -81,9 +85,135 @@ func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { return fmt.Errorf("CRL signature verification failed: %v", err) } + // check extensions + for _, ext := range crl.Extensions { + if ext.Critical { + return fmt.Errorf("CRL contains unsupported critical extension: %v", ext.Id) + } + + // check freshest CRL + if ext.Id.Equal(oidFreshestCRL) { + + } + } + return nil } +func deltaCRL(crl *x509.RevocationList, issuer *x509.Certificate, crlClient CRLClient) (*x509.RevocationList, error) { + for _, ext := range crl.Extensions { + if ext.Id.Equal(oidFreshestCRL) { + url := string(ext.Value) + deltaCDPs, err := parseCDP(ext) + if err != nil { + return nil, err + } + + for _, deltaCDP := range deltaCDPs { + + deltaCRL, err := crlClient.Fetch(url) + if err != nil { + return nil, err + } + + if err := validateCRL(deltaCRL, issuer); err != nil { + return nil, err + } + + if err := validateDeltaCRL(deltaCRL, crl); err != nil { + return nil, err + } + return deltaCRL, nil + + } + } + } + return nil, noDeltaCRL{} +} + +func parseCDP(ext pkix.Extension) ([]string, error) { + var deltaCDPs []string + // RFC 5280, 4.2.1.13 + + // CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint + // + // DistributionPoint ::= SEQUENCE { + // distributionPoint [0] DistributionPointName OPTIONAL, + // reasons [1] ReasonFlags OPTIONAL, + // cRLIssuer [2] GeneralNames OPTIONAL } + // + // DistributionPointName ::= CHOICE { + // fullName [0] GeneralNames, + // nameRelativeToCRLIssuer [1] RelativeDistinguishedName } + val := cryptobyte.String(ext.Value) + if !val.ReadASN1(&val, cryptobyte_asn1.SEQUENCE) { + return nil, errors.New("x509: invalid CRL distribution points") + } + for !val.Empty() { + var dpDER cryptobyte.String + if !val.ReadASN1(&dpDER, cryptobyte_asn1.SEQUENCE) { + return nil, errors.New("x509: invalid CRL distribution point") + } + var dpNameDER cryptobyte.String + var dpNamePresent bool + if !dpDER.ReadOptionalASN1(&dpNameDER, &dpNamePresent, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific()) { + return nil, errors.New("x509: invalid CRL distribution point") + } + if !dpNamePresent { + continue + } + if !dpNameDER.ReadASN1(&dpNameDER, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific()) { + return nil, errors.New("x509: invalid CRL distribution point") + } + for !dpNameDER.Empty() { + if !dpNameDER.PeekASN1Tag(cryptobyte_asn1.Tag(6).ContextSpecific()) { + break + } + var uri cryptobyte.String + if !dpNameDER.ReadASN1(&uri, cryptobyte_asn1.Tag(6).ContextSpecific()) { + return nil, errors.New("x509: invalid CRL distribution point") + } + deltaCDPs = append(deltaCDPs, string(uri)) + } + } + + return deltaCDPs, nil +} + +// Reference: https://tools.ietf.org/html/rfc5280#section-5.2.4 +func validateDeltaCRL(deltaCRL *x509.RevocationList, crl *x509.RevocationList) error { + for _, ext := range deltaCRL.Extensions { + if ext.Id.Equal(oidDeltaCRLIndicator) { + // check critical + if !ext.Critical { + return errors.New("delta CRL is not critical") + } + + // check base CRL number + baseCRLNumber := new(big.Int) + if _, err := asn1.Unmarshal(ext.Value, baseCRLNumber); err != nil { + return fmt.Errorf("failed to parse base CRL number: %v", err) + } + if crl.Number.Cmp(baseCRLNumber) >= 0 { + return staledDeltaCRL{} + } + + // TODO: verify issuingDistributionPoint + + // check issuer + if deltaCRL.Issuer.CommonName != crl.Issuer.CommonName { + return fmt.Errorf("delta CRL issuer is not the same as the base CRL issuer: %s != %s", deltaCRL.Issuer.CommonName, crl.Issuer.CommonName) + } + if !bytes.Equal(crl.AuthorityKeyId, deltaCRL.AuthorityKeyId) { + return errors.New("delta CRL is not valid") + } + + return nil + } + } + return errors.New("delta CRL is not valid") +} + // HasCRL checks if the certificate supports CRL. func HasCRL(cert *x509.Certificate) bool { return len(cert.CRLDistributionPoints) > 0 diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 6fa68315..34d214c2 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -30,7 +30,7 @@ func TestValidCert(t *testing.T) { } r := CertCheckStatus(intermediateCert, rootCert, opts) - if r.Err != nil { + if r.Error != nil { t.Fatal(err) } if r.Result != result.ResultOK { @@ -58,7 +58,7 @@ func TestRevoked(t *testing.T) { } r := CertCheckStatus(intermediateCert, rootCert, opts) - if r.Err != nil { + if r.Error != nil { t.Fatal(err) } if r.Result != result.ResultRevoked { @@ -86,7 +86,7 @@ func TestMSCert(t *testing.T) { } r := CertCheckStatus(intermediateCert, rootCert, opts) - if r.Err != nil { + if r.Error != nil { t.Fatal(err) } if r.Result != result.ResultOK { diff --git a/revocation/crl/error.go b/revocation/crl/error.go new file mode 100644 index 00000000..16688d91 --- /dev/null +++ b/revocation/crl/error.go @@ -0,0 +1,13 @@ +package crl + +type staledDeltaCRL struct{} + +func (e staledDeltaCRL) Error() string { + return "delta CRL is staled" +} + +type noDeltaCRL struct{} + +func (e noDeltaCRL) Error() string { + return "no delta CRL found" +} diff --git a/revocation/result/results.go b/revocation/result/results.go index d106cbd4..fddd3e3d 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -14,7 +14,12 @@ // Package result provides general objects that are used across revocation package result -import "strconv" +import ( + "crypto/x509" + "fmt" + "strconv" + "time" +) // Result is a type of enumerated value to help characterize errors. It can be // OK, Unknown, or Revoked @@ -52,7 +57,7 @@ func (r Result) String() string { } } -// ServerResult encapsulates the result for a single server for a single +// ServerResult encapsulates the OCSP result for a single server for a single // certificate in the chain type ServerResult struct { // Result of revocation for this server (Unknown if there is an error which @@ -79,6 +84,68 @@ func NewServerResult(result Result, server string, err error) *ServerResult { } } +// ReasonCode is CRL reason code +type ReasonCode int + +const ( + Unspecified ReasonCode = iota + KeyCompromise + CACompromise + AffiliationChanged + Superseded + CessationOfOperation + CertificateHold + // value 7 is not used + RemoveFromCRL ReasonCode = iota + 1 + PrivilegeWithdrawn + AACompromise +) + +// String provides a conversion from a ReasonCode to a string +func (r ReasonCode) String() string { + switch r { + case Unspecified: + return "Unspecified" + case KeyCompromise: + return "KeyCompromise" + case CACompromise: + return "CACompromise" + case AffiliationChanged: + return "AffiliationChanged" + case Superseded: + return "Superseded" + case CessationOfOperation: + return "CessationOfOperation" + case CertificateHold: + return "CertificateHold" + case RemoveFromCRL: + return "RemoveFromCRL" + case PrivilegeWithdrawn: + return "PrivilegeWithdrawn" + case AACompromise: + return "AACompromise" + default: + return fmt.Sprintf("invalid reason code with value: %d", r) + } +} + +// CRLStatus encapsulates the result of a CRL check +type CRLStatus struct { + // ReasonCode is the reason code for the CRL status + ReasonCode ReasonCode + + // RevocationTime is the time at which the certificate was revoked + RevocationTime time.Time +} + +// NewCRLStatus creates a CRLStatus object +func NewCRLStatus(entity x509.RevocationListEntry) *CRLStatus { + return &CRLStatus{ + ReasonCode: ReasonCode(entity.ReasonCode), + RevocationTime: entity.RevocationTime, + } +} + // CertRevocationResult encapsulates the result for a single certificate in the // chain as well as the results from individual servers associated with this // certificate @@ -101,5 +168,9 @@ type CertRevocationResult struct { // status from being retrieved. These are all contained here for evaluation ServerResults []*ServerResult - Err error + // CRLStatus is the result of the CRL check for this certificate + CRLStatus *CRLStatus + + // Error is set if there is an error associated with the revocation check + Error error } diff --git a/revocation/revocation.go b/revocation/revocation.go index d8917b28..439bb3f5 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -40,7 +40,7 @@ type Revocation interface { type revocation struct { httpClient *http.Client - // CRLCache caches the CRL files, the default one is memory cache + // CRLCache caches the CRL files; the default one is memory cache CRLCache crl.Cache } From a262babf874f256c0af257b15cc8d61719c79ed0 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 8 Jul 2024 11:29:01 +0800 Subject: [PATCH 006/110] fix: update crl Signed-off-by: Junjie Gao --- revocation/crl/client.go | 5 +- revocation/crl/crl.go | 129 ------------------ ...81926f77973c06922d71f833ead6bef33c396db644 | Bin 814 -> 814 bytes ...13813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b | Bin 607 -> 607 bytes 4 files changed, 3 insertions(+), 131 deletions(-) diff --git a/revocation/crl/client.go b/revocation/crl/client.go index b9264205..45df7c67 100644 --- a/revocation/crl/client.go +++ b/revocation/crl/client.go @@ -41,11 +41,12 @@ func (c *crlClient) Fetch(url string) (*x509.RevocationList, error) { crl, err := x509.ParseRevocationList(data) if err != nil { - return nil, err + // cache is broken + return c.update(url) } if crl.NextUpdate.Before(time.Now()) { - // cache is expired, update + // cache is expired return c.update(url) } diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 210708e3..7b1657ac 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -1,24 +1,13 @@ package crl import ( - "bytes" "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" "errors" "fmt" - "math/big" "net/http" "time" "github.com/notaryproject/notation-core-go/revocation/result" - "golang.org/x/crypto/cryptobyte" - cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" -) - -var ( - oidDeltaCRLIndicator = asn1.ObjectIdentifier{2, 5, 29, 27} - oidFreshestCRL = asn1.ObjectIdentifier{2, 5, 29, 46} ) // Options specifies values that are needed to check OCSP revocation @@ -91,129 +80,11 @@ func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { return fmt.Errorf("CRL contains unsupported critical extension: %v", ext.Id) } - // check freshest CRL - if ext.Id.Equal(oidFreshestCRL) { - - } } return nil } -func deltaCRL(crl *x509.RevocationList, issuer *x509.Certificate, crlClient CRLClient) (*x509.RevocationList, error) { - for _, ext := range crl.Extensions { - if ext.Id.Equal(oidFreshestCRL) { - url := string(ext.Value) - deltaCDPs, err := parseCDP(ext) - if err != nil { - return nil, err - } - - for _, deltaCDP := range deltaCDPs { - - deltaCRL, err := crlClient.Fetch(url) - if err != nil { - return nil, err - } - - if err := validateCRL(deltaCRL, issuer); err != nil { - return nil, err - } - - if err := validateDeltaCRL(deltaCRL, crl); err != nil { - return nil, err - } - return deltaCRL, nil - - } - } - } - return nil, noDeltaCRL{} -} - -func parseCDP(ext pkix.Extension) ([]string, error) { - var deltaCDPs []string - // RFC 5280, 4.2.1.13 - - // CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint - // - // DistributionPoint ::= SEQUENCE { - // distributionPoint [0] DistributionPointName OPTIONAL, - // reasons [1] ReasonFlags OPTIONAL, - // cRLIssuer [2] GeneralNames OPTIONAL } - // - // DistributionPointName ::= CHOICE { - // fullName [0] GeneralNames, - // nameRelativeToCRLIssuer [1] RelativeDistinguishedName } - val := cryptobyte.String(ext.Value) - if !val.ReadASN1(&val, cryptobyte_asn1.SEQUENCE) { - return nil, errors.New("x509: invalid CRL distribution points") - } - for !val.Empty() { - var dpDER cryptobyte.String - if !val.ReadASN1(&dpDER, cryptobyte_asn1.SEQUENCE) { - return nil, errors.New("x509: invalid CRL distribution point") - } - var dpNameDER cryptobyte.String - var dpNamePresent bool - if !dpDER.ReadOptionalASN1(&dpNameDER, &dpNamePresent, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific()) { - return nil, errors.New("x509: invalid CRL distribution point") - } - if !dpNamePresent { - continue - } - if !dpNameDER.ReadASN1(&dpNameDER, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific()) { - return nil, errors.New("x509: invalid CRL distribution point") - } - for !dpNameDER.Empty() { - if !dpNameDER.PeekASN1Tag(cryptobyte_asn1.Tag(6).ContextSpecific()) { - break - } - var uri cryptobyte.String - if !dpNameDER.ReadASN1(&uri, cryptobyte_asn1.Tag(6).ContextSpecific()) { - return nil, errors.New("x509: invalid CRL distribution point") - } - deltaCDPs = append(deltaCDPs, string(uri)) - } - } - - return deltaCDPs, nil -} - -// Reference: https://tools.ietf.org/html/rfc5280#section-5.2.4 -func validateDeltaCRL(deltaCRL *x509.RevocationList, crl *x509.RevocationList) error { - for _, ext := range deltaCRL.Extensions { - if ext.Id.Equal(oidDeltaCRLIndicator) { - // check critical - if !ext.Critical { - return errors.New("delta CRL is not critical") - } - - // check base CRL number - baseCRLNumber := new(big.Int) - if _, err := asn1.Unmarshal(ext.Value, baseCRLNumber); err != nil { - return fmt.Errorf("failed to parse base CRL number: %v", err) - } - if crl.Number.Cmp(baseCRLNumber) >= 0 { - return staledDeltaCRL{} - } - - // TODO: verify issuingDistributionPoint - - // check issuer - if deltaCRL.Issuer.CommonName != crl.Issuer.CommonName { - return fmt.Errorf("delta CRL issuer is not the same as the base CRL issuer: %s != %s", deltaCRL.Issuer.CommonName, crl.Issuer.CommonName) - } - if !bytes.Equal(crl.AuthorityKeyId, deltaCRL.AuthorityKeyId) { - return errors.New("delta CRL is not valid") - } - - return nil - } - } - return errors.New("delta CRL is not valid") -} - // HasCRL checks if the certificate supports CRL. func HasCRL(cert *x509.Certificate) bool { return len(cert.CRLDistributionPoints) > 0 diff --git a/revocation/crl/testdata/cache/18d02b1ec810770d6310fa81926f77973c06922d71f833ead6bef33c396db644 b/revocation/crl/testdata/cache/18d02b1ec810770d6310fa81926f77973c06922d71f833ead6bef33c396db644 index bfde1f141cdfaa278498d5356d7d59b7cdfcfb7a..720e0f5c40f64b7b8db6535bfee70afa4cee3a73 100644 GIT binary patch delta 601 zcmV-f0;c`02CfE>ofkARH!(RgF*7z=7Y#BrFgG$cFgG($0R#bp zHx&c~4;KwGG%zb=0!I_%Ie@|mkLk^r3_!)2(OZh77H2)Kv!&)>hOti)&KYDM| z3iU|;?%?6`_*~`*pE&-~8{rq!O}HtQm0#J+xyu;T7i5SfNRsBThl2q#Z*@FH!j^QO z6|-!GNWm{>aUu)l=4#^8Q*VdNtRcpRX#%(f;(o=}fj9gT2P(6*!+XdA z$_GjD+zdJyEce9YB34v7*?sM8m*f*bV9&g2Z%ObfNz;bbPts8rzaN4Z_3I?fHDsZ~Md$n@<7#lh)+ZQ)v_)3$O9izx1 zP_nJj&3n#mc|O6_g5}OrU~lk!Xl4Crg3xju@$HH*B8n(x*u}(Jb!B)h?Su{Weer}) zxlNUB3EtAb94jFI2~|R8Pj;cYIZX~<{~&SE4ZZYwHzNn}>y?BN{^k$&xQbz&+3O?W n4j_LvLDL0ofk4OF)}bSF)}t<7Y#BrFf}nUFgP_aGLf`Lf08g91_>($0R#bp zHx&c~4;KwGG%z(WF)}bTF)}t$h<3rdZE|NlW> zHo_Z6sH}UT50lLSUWhvx18Ty=ek-yA<;9U!@rNqoo5C7HJ6})F1@hvQJ}ZcO8c0w< zts|`ie>XT+S~nHSAx32cI=Rf7 z&7)ZX5%--AQ%2-%=6`3<(KdKdF->j!h<)-Fwxga{t#B@A;n(C=73NHz@%`9;Q^|tW zt&dH{*~YV3O0U~g$?Iz&ysrswEn(-!bW5&SDkGlpEM$ad?3D4zFFE-ap2DylfPwO< z399vz@H%cwe{@kzy3BcZH%3Fl7iYKezO-lu{xOe+55X#b5Y0g{0=rGNr3zu|wBUuM zBJR}ukA0}mbg{GF6Tfndz844#bYYc{2vJ(MbQ~;t5`mPu85=Ytl?yU2t%4fz7+ryq zNq`%&?gMmEE~N_}s_}{Eij?CZp@4Fiaw@5j3>?D(H?ypy9%JzrFHAlGyX!0uL%>#i n67;oaHIemv)4(z%O9VCW zH?>~*8W_meygeBJBY*Iy1BT+mWO@NLdg4e%60$JE&+zS|aA$(7Pcn1nkyk;u2(0PO z_eO;du#RXUqWqudP)na^H0g2j=u}B<9m($u$gBLq#TS9BC>%}lt=)A4a;nSu M>`y~|*x0Pb5Y9W3VgLXD delta 314 zcmV-A0mc5`1m6UZoES7QH!(CZFf=n-7Y#BrFf=kWF*GqSG&7O3mVb&c4F(A+hDe6@ z4FLfQ1potr0RaG2JEPY4C0mYeLD9>RmfrV9KNqhGbUp=9z0zrXtpXi9qQx*%UAM5| zh+^DC)c+ANZ;zGCqbOF}(>3hyCQNeg6lS(5O0=ic`PWH~4U04TfuS*x59v^-!A1J( zOLwy(mVJHS!3`8NQGZs#(5a6;=L`tkG-m9lr3SCuHi0_QU3Z&nXpCsX|*!N=YG;7LuBjYyl9vvvS*x`J8aW!L4878e`mGKUeMEXyOd>uSSS MOVJ4}IIIa`yM^hH4gdfE From e66d9fdb9953246389816e4d40ebbe9f924414f8 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 11 Jul 2024 16:59:10 +0800 Subject: [PATCH 007/110] fix: update cache Signed-off-by: Junjie Gao --- revocation/crl/cache.go | 99 +++++----- revocation/crl/client.go | 76 -------- revocation/crl/crl.go | 14 +- revocation/crl/crlStore.go | 174 ++++++++++++++++++ revocation/crl/error.go | 12 -- revocation/crl/fetcher.go | 76 ++++++++ ...81926f77973c06922d71f833ead6bef33c396db644 | Bin 814 -> 0 bytes ...b898c09e6820b06260002b8deafaef94f9a4f79ff4 | Bin 717 -> 0 bytes ...13813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b | Bin 607 -> 0 bytes 9 files changed, 312 insertions(+), 139 deletions(-) delete mode 100644 revocation/crl/client.go create mode 100644 revocation/crl/crlStore.go create mode 100644 revocation/crl/fetcher.go delete mode 100644 revocation/crl/testdata/cache/18d02b1ec810770d6310fa81926f77973c06922d71f833ead6bef33c396db644 delete mode 100644 revocation/crl/testdata/cache/69b1d41239160438fedb94b898c09e6820b06260002b8deafaef94f9a4f79ff4 delete mode 100644 revocation/crl/testdata/cache/fdd0c186cdf0cf45ef531213813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b diff --git a/revocation/crl/cache.go b/revocation/crl/cache.go index 3789c25d..39ae0bd8 100644 --- a/revocation/crl/cache.go +++ b/revocation/crl/cache.go @@ -1,84 +1,89 @@ package crl import ( - "crypto/sha256" - "encoding/hex" + "io" "os" "path/filepath" - "sync" ) +const tempFileName = "notation-*.crl" + type Cache interface { - // Get retrieves the CRL from the store - Get(key string) ([]byte, error) + Get(key string) (io.ReadCloser, error) + + Set(key string) (WriteCanceler, error) - // Set stores the CRL in the store - Set(key string, value []byte) error + Delete(key string) error +} +type WriteCanceler interface { + io.WriteCloser + Cancel() } +// fileSystemCache builds on top of OS file system to leverage the file system +// concurrency control and atomicity type fileSystemCache struct { dir string - - // mu protects the cache - mu sync.RWMutex } // NewFileSystemCache creates a new file system store func NewFileSystemCache(dir string) Cache { return &fileSystemCache{ dir: dir, - mu: sync.RWMutex{}, } } -func (f *fileSystemCache) Get(key string) ([]byte, error) { - f.mu.RLock() - defer f.mu.RUnlock() - - fileName := hashURL(key) - return os.ReadFile(filepath.Join(f.dir, fileName)) +// Get retrieves the CRL from the store +func (f *fileSystemCache) Get(fileName string) (io.ReadCloser, error) { + return os.Open(filepath.Join(f.dir, fileName)) } -// Set stores the CRL in the store. It hashes the URL to determine the -// filename to store the CRL in. -func (f *fileSystemCache) Set(key string, value []byte) error { - f.mu.Lock() - defer f.mu.Unlock() - - fileName := hashURL(key) - return os.WriteFile(filepath.Join(f.dir, fileName), value, 0644) +// Set stores the CRL in the store +func (f *fileSystemCache) Set(filename string) (WriteCanceler, error) { + return newFileSystemWriter(filepath.Join(f.dir, filename)) } -type memeoryCache struct { - cache map[string][]byte +func (f *fileSystemCache) Delete(fileName string) error { + return os.Remove(filepath.Join(f.dir, fileName)) +} - // mu protects the cache - mu sync.RWMutex +// fileSystemWriter is a WriteCanceler implementation that writes to +// a file system file and renames it to the final path when Close is called +type fileSystemWriter struct { + io.WriteCloser + tempFilePath string + filePath string + canceled bool } -// NewMemoryCache creates a new memory store -func NewMemoryCache() Cache { - return &memeoryCache{ - cache: make(map[string][]byte), - mu: sync.RWMutex{}, +func newFileSystemWriter(filePath string) (WriteCanceler, error) { + tempFile, err := os.CreateTemp("", tempFileName) + if err != nil { + return nil, err } + + return &fileSystemWriter{ + WriteCloser: tempFile, + tempFilePath: tempFile.Name(), + filePath: filePath, + }, nil } -func (m *memeoryCache) Get(key string) ([]byte, error) { - m.mu.RLock() - defer m.mu.RUnlock() - return m.cache[key], nil +func (c *fileSystemWriter) Write(p []byte) (int, error) { + return c.WriteCloser.Write(p) } -func (m *memeoryCache) Set(key string, value []byte) error { - m.mu.Lock() - defer m.mu.Unlock() - m.cache[key] = value - return nil +func (c *fileSystemWriter) Cancel() { + c.canceled = true } -// hashURL hashes the URL with SHA256 and returns the hex-encoded result -func hashURL(url string) string { - hash := sha256.Sum256([]byte(url)) - return hex.EncodeToString(hash[:]) +func (c *fileSystemWriter) Close() error { + if err := c.WriteCloser.Close(); err != nil { + return err + } + + if !c.canceled { + return os.Rename(c.tempFilePath, c.filePath) + } + return nil } diff --git a/revocation/crl/client.go b/revocation/crl/client.go deleted file mode 100644 index 45df7c67..00000000 --- a/revocation/crl/client.go +++ /dev/null @@ -1,76 +0,0 @@ -package crl - -import ( - "crypto/x509" - "io" - "net/http" - "os" - "time" -) - -// CRLClient is an interface to fetch CRL -type CRLClient interface { - // Fetch retrieves the CRL with the given URL from remote or local cache - Fetch(url string) (*x509.RevocationList, error) -} - -type crlClient struct { - cache Cache - httpClient *http.Client -} - -// NewCRLClient creates a new CRL server -func NewCRLClient(cache Cache, httpClient *http.Client) CRLClient { - return &crlClient{ - cache: cache, - httpClient: httpClient, - } -} - -func (c *crlClient) Fetch(url string) (*x509.RevocationList, error) { - // try to get from cache - data, err := c.cache.Get(url) - if err != nil { - if os.IsNotExist(err) { - // fallback to fetch from remote - return c.update(url) - } - - return nil, err - } - - crl, err := x509.ParseRevocationList(data) - if err != nil { - // cache is broken - return c.update(url) - } - - if crl.NextUpdate.Before(time.Now()) { - // cache is expired - return c.update(url) - } - - return crl, nil -} - -func (c *crlClient) update(url string) (*x509.RevocationList, error) { - // fetch from remote - resp, err := c.httpClient.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - // Read the CRL file - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - // store in cache - if err := c.cache.Set(url, data); err != nil { - return nil, err - } - - return x509.ParseRevocationList(data) -} diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 7b1657ac..f675f39e 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -28,25 +28,31 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR return &result.CertRevocationResult{Error: errors.New("certificate does not support CRL")} } - crlClient := NewCRLClient(opts.Cache, opts.HTTPClient) + crlFetcher := NewCachedCRLFetcher(opts.HTTPClient, opts.Cache) // Check CRL var lastError error for _, crlURL := range cert.CRLDistributionPoints { - crl, err := crlClient.Fetch(crlURL) + crlStore, err := crlFetcher.Fetch(crlURL) if err != nil { lastError = err continue } - err = validateCRL(crl, issuer) + err = validateCRL(crlStore.BaseCRL(), issuer) if err != nil { lastError = err continue } + // cache + if err := crlStore.Save(); err != nil { + lastError = err + continue + } + // check revocation - for _, revokedCert := range crl.RevokedCertificateEntries { + for _, revokedCert := range crlStore.BaseCRL().RevokedCertificateEntries { if revokedCert.SerialNumber.Cmp(cert.SerialNumber) == 0 { return &result.CertRevocationResult{ Result: result.ResultRevoked, diff --git a/revocation/crl/crlStore.go b/revocation/crl/crlStore.go new file mode 100644 index 00000000..ea902325 --- /dev/null +++ b/revocation/crl/crlStore.go @@ -0,0 +1,174 @@ +package crl + +import ( + "archive/tar" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "time" +) + +const BaseCRL = "base.crl" +const Metadata = "metadata.json" + +type CRLStore interface { + BaseCRL() *x509.RevocationList + Metadata() map[string]string + SetBaseCRL(baseCRL *x509.RevocationList, url string) + Save() error +} + +type crlTarStore struct { + baseCRL *x509.RevocationList + metadata map[string]string + + cache Cache +} + +func NewCRLTarStore(baseCRL *x509.RevocationList, url string, cache Cache) CRLStore { + return &crlTarStore{ + baseCRL: baseCRL, + metadata: map[string]string{BaseCRL: url}, + cache: cache} +} + +// ParseCRLTar parses the CRL tarball +func ParseCRLTar(data io.Reader) (*crlTarStore, error) { + CRLTar := &crlTarStore{} + + // parse the tarball + tar := tar.NewReader(data) + + for { + header, err := tar.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + switch header.Name { + case BaseCRL: + // parse base.crl + data, err := io.ReadAll(tar) + if err != nil { + return nil, err + } + + var baseCRL *x509.RevocationList + baseCRL, err = x509.ParseRevocationList(data) + if err != nil { + return nil, err + } + + CRLTar.baseCRL = baseCRL + case Metadata: + // parse metadata + metadata := make(map[string]string) + if err := json.NewDecoder(tar).Decode(&metadata); err != nil { + return nil, err + } + + CRLTar.metadata = metadata + + default: + return nil, fmt.Errorf("unknown file in tarball: %s", header.Name) + } + } + + if CRLTar.baseCRL == nil { + return nil, errors.New("base.crl is missing") + } + + if CRLTar.metadata == nil { + return nil, errors.New("metadata.json is missing") + } + + if _, ok := CRLTar.metadata[BaseCRL]; !ok { + return nil, errors.New("base.crl URL is missing") + } + + return CRLTar, nil +} + +func (c *crlTarStore) BaseCRL() *x509.RevocationList { + return c.baseCRL +} + +func (c *crlTarStore) Metadata() map[string]string { + return c.metadata +} + +func (c *crlTarStore) SetBaseCRL(baseCRL *x509.RevocationList, url string) { + c.baseCRL = baseCRL + + if c.metadata == nil { + c.metadata = make(map[string]string) + } + c.metadata[BaseCRL] = url +} + +func (c *crlTarStore) Save() error { + baseCRLURL, ok := c.metadata[BaseCRL] + if !ok { + return errors.New("base.crl URL is missing") + } + + // create cache file + w, err := c.cache.Set(buildTarName(baseCRLURL)) + if err != nil { + return err + } + defer w.Close() + + if err := c.saveTar(w); err != nil { + w.Cancel() + return err + } + + return nil +} + +func (c *crlTarStore) saveTar(w WriteCanceler) error { + tarWriter := tar.NewWriter(w) + // Add base.crl + if err := addToTar(BaseCRL, c.baseCRL.Raw, tarWriter); err != nil { + return err + } + + // Add metadataBytes.json + metadataBytes, err := json.Marshal(c.metadata) + if err != nil { + return err + } + return addToTar(Metadata, metadataBytes, tarWriter) +} + +func buildTarName(url string) string { + return hashURL(url) + ".tar" +} + +// hashURL hashes the URL with SHA256 and returns the hex-encoded result +func hashURL(url string) string { + hash := sha256.Sum256([]byte(url)) + return hex.EncodeToString(hash[:]) +} + +func addToTar(fileName string, data []byte, tw *tar.Writer) error { + header := &tar.Header{ + Name: fileName, + Size: int64(len(data)), + Mode: 0644, + ModTime: time.Now(), + } + if err := tw.WriteHeader(header); err != nil { + return err + } + _, err := tw.Write(data) + return err +} diff --git a/revocation/crl/error.go b/revocation/crl/error.go index 16688d91..d65b2a07 100644 --- a/revocation/crl/error.go +++ b/revocation/crl/error.go @@ -1,13 +1 @@ package crl - -type staledDeltaCRL struct{} - -func (e staledDeltaCRL) Error() string { - return "delta CRL is staled" -} - -type noDeltaCRL struct{} - -func (e noDeltaCRL) Error() string { - return "no delta CRL found" -} diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go new file mode 100644 index 00000000..9a4b6b2e --- /dev/null +++ b/revocation/crl/fetcher.go @@ -0,0 +1,76 @@ +package crl + +import ( + "crypto/x509" + "io" + "net/http" + "os" + "time" +) + +// CRLFetcher is an interface to fetch CRL +type CRLFetcher interface { + // Fetch retrieves the CRL with the given URL + Fetch(url string) (CRLStore, error) +} + +type cachedCRLFetcher struct { + httpClient *http.Client + cache Cache +} + +// NewCachedCRLFetcher creates a new CRL fetcher with cache +func NewCachedCRLFetcher(httpClient *http.Client, cache Cache) CRLFetcher { + return &cachedCRLFetcher{ + httpClient: httpClient, + cache: cache, + } +} + +func (c *cachedCRLFetcher) Fetch(url string) (CRLStore, error) { + // try to get from cache + file, err := c.cache.Get(url) + if err != nil { + if os.IsNotExist(err) { + // fallback to fetch from remote + return c.download(url) + } + + return nil, err + } + defer file.Close() + + crlStore, err := ParseCRLTar(file) + if err != nil { + return c.download(url) + } + + if crlStore.baseCRL.NextUpdate.Before(time.Now()) { + // cache is expired + return c.download(url) + } + + return crlStore, nil +} + +func (c *cachedCRLFetcher) download(url string) (CRLStore, error) { + // fetch from remote + resp, err := c.httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + crl, err := x509.ParseRevocationList(data) + if err != nil { + return nil, err + } + + crlStore := NewCRLTarStore(crl, url, c.cache) + return crlStore, nil +} diff --git a/revocation/crl/testdata/cache/18d02b1ec810770d6310fa81926f77973c06922d71f833ead6bef33c396db644 b/revocation/crl/testdata/cache/18d02b1ec810770d6310fa81926f77973c06922d71f833ead6bef33c396db644 deleted file mode 100644 index 720e0f5c40f64b7b8db6535bfee70afa4cee3a73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 814 zcmXqLV%9QfViaOxWHjJqsB4HrbATocsXe>iqQf%{=rw4*FZ>2kv;K8%=dEP&dHH0I=5w(_>sMXM%Q$Z(&$mM)xPOvV zsrF_5_dD5Em#p}zq5SyS9yKnN3Fh0M9h^DqSiOI8pfleb(I1ipV%|Ts-kAIso^v?L zM9*i-F;)Aj{L5TlJ^sIY@bL4Gm}i{xE&g4Sekgv$cZcTGsqr_@>^v=TMLeZL)nn4L z4eia0#`&c-u7{?T%op99)9P_hKfO?i`{}c+N0)=A$mvY(JgAm*>Da^R5+C+hzxnQV zeEU6{%;nkJjlTRlC!HDIvLWe_uEBo38EfZB<~XKv^S@ax{q1PLn%7U>u3e*etSysi z2kWD{qt_bE{|K>bZQgpg`UKM{cF&J@c&sFKz8`t46c%E2v-WN7^e4g&3Fr4@<$He6 z^1R%3&HqxM_DWT#sbW}eAc+oqrKvAXzW7oS$a zt^?Iui+)K+Tj|^uH&6NDHF=us;uA^%o7P@DTYWCK((d5Zrl;qE6Y@XQW~BbjY`Rb+ z`|)j;fl`-7+Kr<}qDoUMbl>poGZ4vzUjQ{(NuEe=FUn@W4Q>Zs{yv*7%P5XR%{!iaqC*EyrzWt!| Ro{ca|M)h7VjsNGmN&yZWL_7ch diff --git a/revocation/crl/testdata/cache/69b1d41239160438fedb94b898c09e6820b06260002b8deafaef94f9a4f79ff4 b/revocation/crl/testdata/cache/69b1d41239160438fedb94b898c09e6820b06260002b8deafaef94f9a4f79ff4 deleted file mode 100644 index f7df094da08897c1942d7ad9a608b81c8d544867..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 717 zcmV;;0y6zDf&$4ffwBSt0Wb{)2`Yw2hW8Bt0Sg5HFi$ZHFbM_&RRjhT0##EnDKIAn z162eH6Cg=$bY*gGWpp4@Wn*=6X>@rYQe|^xVRB<=AV+dGoDJ>9*D<6j{P|gxv#s&T;zB>NAtcGc*G-P!D{J_Wiguo zBoC1wm>)+~%ByN}t*p7g`8-b>Mp)QTkLMG6|NCEy!MXvLH+M83sMSrdobr!tBC_3I z1-2~SnWDDp?^$Hk!LWaTMa*c2d}Hfl2k}W7k2vH7EqvfYn63v0i}##6sR?kuk5bbx z>fb_Vh-=c`t34>Bde?osf^TXNmH|pXf!E>VT9H_uE%NzZSkKO5qrFCt43}(Am(@t# zoXc0kn#Q7%c;*E?)lL-I`1A>$(*6@TTj{R~?2_ld?piZQ?lFSbZwt-rs@rCt*r8(} zS9^A<(AK3M$wWonJQ%%i1-Kblpy|bmSa3V!BEJpDqu^7fe=DXr6s;=_w@OCRcp+zN zB{L(|s~3Q~W+LqFZL#G6u1;HAJ2g~3T9i@sf$Pf<%GNo47q0M4C4pqkwgw66b?XiW z<)pwK4kVv5zx^K)NLOItr0A-$FzM{zQg%d6y+Ubewcb(hgrpW3Nun##Jz}(O(7iO0 diff --git a/revocation/crl/testdata/cache/fdd0c186cdf0cf45ef531213813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b b/revocation/crl/testdata/cache/fdd0c186cdf0cf45ef531213813eebaaefcfe10b1d0dd8bf3a144164d3c93d4b deleted file mode 100644 index d749fdff46d296d31e789d16ee9fcbc0750cc34b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 607 zcmXqLVv06sVsvI=WHjJq~179`@kGoV5I+6hkQk36KaEkDzmYZhlI>g0rK7 zPiAgrNotCrqJca}j+sZo(a}-CIkl)HGc7Y2Xr@ANYEfBca%!EHE(813ObznMJ}ttU+XfAeXY;w2$m>9wQakp96s z=}Pj$U7e=qcARDSS$O$?k*YU~=?C+zaX+ObPF&w(E6JezVFh#Bqr)jxjHXqOJlupf z85}h+PkI9MXOH#eB Date: Fri, 12 Jul 2024 15:24:37 +0800 Subject: [PATCH 008/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/cache.go | 18 ++++++ revocation/crl/cache/dummy.go | 45 +++++++++++++++ revocation/crl/{cache.go => cache/fs.go} | 14 +---- revocation/crl/crl.go | 15 ++--- revocation/crl/crlStore.go | 8 ++- revocation/crl/crl_test.go | 7 ++- revocation/crl/fetcher.go | 35 +++++++----- revocation/revocation.go | 72 ++++++++++++++++++++++-- 8 files changed, 168 insertions(+), 46 deletions(-) create mode 100644 revocation/crl/cache/cache.go create mode 100644 revocation/crl/cache/dummy.go rename revocation/crl/{cache.go => cache/fs.go} (89%) diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go new file mode 100644 index 00000000..71db39b1 --- /dev/null +++ b/revocation/crl/cache/cache.go @@ -0,0 +1,18 @@ +package cache + +import ( + "io" +) + +type Cache interface { + Get(key string) (io.ReadCloser, error) + + Set(key string) (WriteCanceler, error) + + Delete(key string) error +} + +type WriteCanceler interface { + io.WriteCloser + Cancel() +} diff --git a/revocation/crl/cache/dummy.go b/revocation/crl/cache/dummy.go new file mode 100644 index 00000000..f3e1f240 --- /dev/null +++ b/revocation/crl/cache/dummy.go @@ -0,0 +1,45 @@ +package cache + +import ( + "io" + "os" +) + +// dummyCache is a dummy cache implementation that does nothing +type dummyCache struct { +} + +// NewDummyCache creates a new dummy cache +func NewDummyCache() Cache { + return &dummyCache{} +} + +// Get retrieves the CRL from the store +func (d *dummyCache) Get(fileName string) (io.ReadCloser, error) { + return nil, os.ErrNotExist +} + +// Set stores the CRL in the store +func (d *dummyCache) Set(filename string) (WriteCanceler, error) { + return &dummyWriter{}, nil +} + +func (d *dummyCache) Delete(fileName string) error { + return nil +} + +// dummyWriter is a WriteCanceler implementation that writes to +// a bytes.Buffer +type dummyWriter struct { +} + +func (d *dummyWriter) Write(p []byte) (int, error) { + return len(p), nil +} + +func (d *dummyWriter) Cancel() { +} + +func (d *dummyWriter) Close() error { + return nil +} diff --git a/revocation/crl/cache.go b/revocation/crl/cache/fs.go similarity index 89% rename from revocation/crl/cache.go rename to revocation/crl/cache/fs.go index 39ae0bd8..274449ea 100644 --- a/revocation/crl/cache.go +++ b/revocation/crl/cache/fs.go @@ -1,4 +1,4 @@ -package crl +package cache import ( "io" @@ -8,18 +8,6 @@ import ( const tempFileName = "notation-*.crl" -type Cache interface { - Get(key string) (io.ReadCloser, error) - - Set(key string) (WriteCanceler, error) - - Delete(key string) error -} -type WriteCanceler interface { - io.WriteCloser - Cancel() -} - // fileSystemCache builds on top of OS file system to leverage the file system // concurrency control and atomicity type fileSystemCache struct { diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index f675f39e..b10be551 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/result" ) @@ -14,7 +15,7 @@ import ( type Options struct { CertChain []*x509.Certificate HTTPClient *http.Client - Cache Cache + Cache cache.Cache } func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { @@ -33,7 +34,7 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR // Check CRL var lastError error for _, crlURL := range cert.CRLDistributionPoints { - crlStore, err := crlFetcher.Fetch(crlURL) + crlStore, cached, err := crlFetcher.Fetch(crlURL) if err != nil { lastError = err continue @@ -45,10 +46,11 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR continue } - // cache - if err := crlStore.Save(); err != nil { - lastError = err - continue + if !cached { + if err := crlStore.Save(); err != nil { + lastError = err + continue + } } // check revocation @@ -85,7 +87,6 @@ func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { if ext.Critical { return fmt.Errorf("CRL contains unsupported critical extension: %v", ext.Id) } - } return nil diff --git a/revocation/crl/crlStore.go b/revocation/crl/crlStore.go index ea902325..d1432992 100644 --- a/revocation/crl/crlStore.go +++ b/revocation/crl/crlStore.go @@ -10,6 +10,8 @@ import ( "fmt" "io" "time" + + "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) const BaseCRL = "base.crl" @@ -26,10 +28,10 @@ type crlTarStore struct { baseCRL *x509.RevocationList metadata map[string]string - cache Cache + cache cache.Cache } -func NewCRLTarStore(baseCRL *x509.RevocationList, url string, cache Cache) CRLStore { +func NewCRLTarStore(baseCRL *x509.RevocationList, url string, cache cache.Cache) CRLStore { return &crlTarStore{ baseCRL: baseCRL, metadata: map[string]string{BaseCRL: url}, @@ -134,7 +136,7 @@ func (c *crlTarStore) Save() error { return nil } -func (c *crlTarStore) saveTar(w WriteCanceler) error { +func (c *crlTarStore) saveTar(w cache.WriteCanceler) error { tarWriter := tar.NewWriter(w) // Add base.crl if err := addToTar(BaseCRL, c.baseCRL.Raw, tarWriter); err != nil { diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 34d214c2..2138336e 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/result" ) @@ -26,7 +27,7 @@ func TestValidCert(t *testing.T) { opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: NewFileSystemCache(filepath.Join("testdata", "cache")), + Cache: cache.NewFileSystemCache(filepath.Join("testdata", "cache")), } r := CertCheckStatus(intermediateCert, rootCert, opts) @@ -54,7 +55,7 @@ func TestRevoked(t *testing.T) { opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: NewFileSystemCache(filepath.Join("testdata", "cache")), + Cache: cache.NewFileSystemCache(filepath.Join("testdata", "cache")), } r := CertCheckStatus(intermediateCert, rootCert, opts) @@ -82,7 +83,7 @@ func TestMSCert(t *testing.T) { opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: NewFileSystemCache(filepath.Join("testdata", "cache")), + Cache: cache.NewFileSystemCache(filepath.Join("testdata", "cache")), } r := CertCheckStatus(intermediateCert, rootCert, opts) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 9a4b6b2e..4fb9da3f 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -5,52 +5,57 @@ import ( "io" "net/http" "os" - "time" + + "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) // CRLFetcher is an interface to fetch CRL type CRLFetcher interface { // Fetch retrieves the CRL with the given URL - Fetch(url string) (CRLStore, error) + Fetch(url string) (crlStore CRLStore, cached bool, err error) } type cachedCRLFetcher struct { httpClient *http.Client - cache Cache + cache cache.Cache } // NewCachedCRLFetcher creates a new CRL fetcher with cache -func NewCachedCRLFetcher(httpClient *http.Client, cache Cache) CRLFetcher { +func NewCachedCRLFetcher(httpClient *http.Client, cache cache.Cache) CRLFetcher { return &cachedCRLFetcher{ httpClient: httpClient, cache: cache, } } -func (c *cachedCRLFetcher) Fetch(url string) (CRLStore, error) { +func (c *cachedCRLFetcher) Fetch(url string) (crlStore CRLStore, cached bool, err error) { // try to get from cache file, err := c.cache.Get(url) if err != nil { if os.IsNotExist(err) { // fallback to fetch from remote - return c.download(url) + crlStore, err := c.download(url) + if err != nil { + return nil, false, err + } + return crlStore, cached, nil } - return nil, err + return nil, false, err } defer file.Close() - crlStore, err := ParseCRLTar(file) + crlStore, err = ParseCRLTar(file) if err != nil { - return c.download(url) - } - - if crlStore.baseCRL.NextUpdate.Before(time.Now()) { - // cache is expired - return c.download(url) + crlStore, err := c.download(url) + if err != nil { + return nil, false, err + } + return crlStore, cached, nil } - return crlStore, nil + cached = true + return crlStore, cached, nil } func (c *cachedCRLFetcher) download(url string) (CRLStore, error) { diff --git a/revocation/revocation.go b/revocation/revocation.go index 98356efb..c4b50673 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -23,6 +23,7 @@ import ( "time" "github.com/notaryproject/notation-core-go/revocation/crl" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/ocsp" "github.com/notaryproject/notation-core-go/revocation/result" coreX509 "github.com/notaryproject/notation-core-go/x509" @@ -36,13 +37,43 @@ type Revocation interface { Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) } +type Mode int + +const ( + // ModeAutoFallback is the default mode that tries OCSP first and falls back + // to CRL if OCSP doesn't exist or fails with unknown status + ModeAutoFallback Mode = iota + + // ModeOCSPOnly is the mode that only uses OCSP for revocation checking + ModeOCSPOnly + + // ModeCRLOnly is the mode that only uses CRL for revocation checking + ModeCRLOnly +) + +func (m Mode) CanRunOCSP() bool { + return m == ModeAutoFallback || m == ModeOCSPOnly +} + +func (m Mode) CanRunCRL() bool { + return m == ModeAutoFallback || m == ModeCRLOnly +} + +type Options struct { + Mode Mode + CertChainPurpose ocsp.Purpose + CRLCache cache.Cache +} + // revocation is an internal struct used for revocation checking type revocation struct { + mode Mode httpClient *http.Client - // CRLCache caches the CRL files; the default one is memory cache - CRLCache crl.Cache certChainPurpose ocsp.Purpose + + // crlCache caches the CRL files; the default one is memory cache + crlCache cache.Cache } // New constructs a revocation object for code signing certificate chain @@ -54,6 +85,7 @@ func New(httpClient *http.Client) (Revocation, error) { return &revocation{ httpClient: httpClient, certChainPurpose: ocsp.PurposeCodeSigning, + crlCache: cache.NewDummyCache(), }, nil } @@ -66,6 +98,36 @@ func NewTimestamp(httpClient *http.Client) (Revocation, error) { return &revocation{ httpClient: httpClient, certChainPurpose: ocsp.PurposeTimestamping, + crlCache: cache.NewDummyCache(), + }, nil +} + +func NewWithOptions(httpClient *http.Client, opts Options) (Revocation, error) { + if httpClient == nil { + return nil, errors.New("invalid input: a non-nil httpClient must be specified") + } + + switch opts.Mode { + case ModeAutoFallback, ModeOCSPOnly, ModeCRLOnly: + default: + return nil, errors.New("invalid input: unknown mode") + } + + switch opts.CertChainPurpose { + case ocsp.PurposeCodeSigning, ocsp.PurposeTimestamping: + default: + return nil, errors.New("invalid input: unknown cert chain purpose") + } + + if opts.CRLCache == nil { + opts.CRLCache = cache.NewDummyCache() + } + + return &revocation{ + mode: opts.Mode, + httpClient: httpClient, + certChainPurpose: opts.CertChainPurpose, + crlCache: opts.CRLCache, }, nil } @@ -91,14 +153,14 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti crlOpts := crl.Options{ CertChain: certChain, HTTPClient: r.httpClient, - Cache: r.CRLCache, + Cache: r.crlCache, } certResults := make([]*result.CertRevocationResult, len(certChain)) var wg sync.WaitGroup for i, cert := range certChain[:len(certChain)-1] { switch { - case ocsp.HasOCSP(cert): + case r.mode.CanRunOCSP() && ocsp.HasOCSP(cert): // do OCSP check for the certificate wg.Add(1) @@ -107,7 +169,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti defer wg.Done() certResults[i] = ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) }(i, cert) - case crl.HasCRL(cert): + case r.mode.CanRunCRL() && crl.HasCRL(cert): // do CRL check for the certificate wg.Add(1) From 31fa5a5ddc494eede367573340977a2ae3a8b19c Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 12 Jul 2024 16:07:59 +0800 Subject: [PATCH 009/110] fix: update test Signed-off-by: Junjie Gao --- revocation/crl/cache/fs.go | 7 +++++- revocation/crl/crlStore.go | 8 +++++-- revocation/crl/crl_test.go | 44 +++++++++++++++++++++++++++----------- revocation/crl/fetcher.go | 2 +- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go index 274449ea..5aba0c32 100644 --- a/revocation/crl/cache/fs.go +++ b/revocation/crl/cache/fs.go @@ -6,7 +6,7 @@ import ( "path/filepath" ) -const tempFileName = "notation-*.crl" +const tempFileName = "notation-*" // fileSystemCache builds on top of OS file system to leverage the file system // concurrency control and atomicity @@ -50,6 +50,11 @@ func newFileSystemWriter(filePath string) (WriteCanceler, error) { return nil, err } + filePath, err = filepath.Abs(filePath) + if err != nil { + return nil, err + } + return &fileSystemWriter{ WriteCloser: tempFile, tempFilePath: tempFile.Name(), diff --git a/revocation/crl/crlStore.go b/revocation/crl/crlStore.go index d1432992..1f7fd0a5 100644 --- a/revocation/crl/crlStore.go +++ b/revocation/crl/crlStore.go @@ -115,7 +115,7 @@ func (c *crlTarStore) SetBaseCRL(baseCRL *x509.RevocationList, url string) { c.metadata[BaseCRL] = url } -func (c *crlTarStore) Save() error { +func (c *crlTarStore) Save() (err error) { baseCRLURL, ok := c.metadata[BaseCRL] if !ok { return errors.New("base.crl URL is missing") @@ -126,7 +126,11 @@ func (c *crlTarStore) Save() error { if err != nil { return err } - defer w.Close() + defer func() { + if cerr := w.Close(); cerr != nil && err == nil { + err = cerr + } + }() if err := c.saveTar(w); err != nil { w.Cancel() diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 2138336e..1eedd847 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -12,6 +12,8 @@ import ( ) func TestValidCert(t *testing.T) { + tempDir := t.TempDir() + // read intermediate cert file intermediateCert, err := loadCertFile(filepath.Join("testdata", "valid", "valid.cer")) if err != nil { @@ -27,19 +29,33 @@ func TestValidCert(t *testing.T) { opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache.NewFileSystemCache(filepath.Join("testdata", "cache")), - } - - r := CertCheckStatus(intermediateCert, rootCert, opts) - if r.Error != nil { - t.Fatal(err) - } - if r.Result != result.ResultOK { - t.Fatal("unexpected result") - } + Cache: cache.NewFileSystemCache(tempDir), + } + + t.Run("validate without cache", func(t *testing.T) { + r := CertCheckStatus(intermediateCert, rootCert, opts) + if r.Error != nil { + t.Fatal(err) + } + if r.Result != result.ResultOK { + t.Fatal("unexpected result") + } + }) + + t.Run("validate with cache", func(t *testing.T) { + r := CertCheckStatus(intermediateCert, rootCert, opts) + if r.Error != nil { + t.Fatal(err) + } + if r.Result != result.ResultOK { + t.Fatal("unexpected result") + } + }) } func TestRevoked(t *testing.T) { + tempDir := t.TempDir() + // read intermediate cert file intermediateCert, err := loadCertFile(filepath.Join("testdata", "revoked", "revoked.cer")) if err != nil { @@ -55,12 +71,12 @@ func TestRevoked(t *testing.T) { opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache.NewFileSystemCache(filepath.Join("testdata", "cache")), + Cache: cache.NewFileSystemCache(tempDir), } r := CertCheckStatus(intermediateCert, rootCert, opts) if r.Error != nil { - t.Fatal(err) + t.Fatal(r.Error) } if r.Result != result.ResultRevoked { t.Fatal("unexpected result") @@ -68,6 +84,8 @@ func TestRevoked(t *testing.T) { } func TestMSCert(t *testing.T) { + tempDir := t.TempDir() + // read intermediate cert file intermediateCert, err := loadCertFile(filepath.Join("testdata", "ms", "msleaf.cer")) if err != nil { @@ -83,7 +101,7 @@ func TestMSCert(t *testing.T) { opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache.NewFileSystemCache(filepath.Join("testdata", "cache")), + Cache: cache.NewFileSystemCache(tempDir), } r := CertCheckStatus(intermediateCert, rootCert, opts) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 4fb9da3f..532409c3 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -30,7 +30,7 @@ func NewCachedCRLFetcher(httpClient *http.Client, cache cache.Cache) CRLFetcher func (c *cachedCRLFetcher) Fetch(url string) (crlStore CRLStore, cached bool, err error) { // try to get from cache - file, err := c.cache.Get(url) + file, err := c.cache.Get(buildTarName(url)) if err != nil { if os.IsNotExist(err) { // fallback to fetch from remote From 039be391edbc8392bb41832e87aa2adaeea0bd11 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 12 Jul 2024 16:29:31 +0800 Subject: [PATCH 010/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 532409c3..8e261bd1 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -38,7 +38,7 @@ func (c *cachedCRLFetcher) Fetch(url string) (crlStore CRLStore, cached bool, er if err != nil { return nil, false, err } - return crlStore, cached, nil + return crlStore, false, nil } return nil, false, err @@ -51,11 +51,10 @@ func (c *cachedCRLFetcher) Fetch(url string) (crlStore CRLStore, cached bool, er if err != nil { return nil, false, err } - return crlStore, cached, nil + return crlStore, false, nil } - cached = true - return crlStore, cached, nil + return crlStore, true, nil } func (c *cachedCRLFetcher) download(url string) (CRLStore, error) { From 3b1a7ed9bf606e2df677b47cae3f37894d69e9b2 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 12 Jul 2024 17:32:06 +0800 Subject: [PATCH 011/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 25 ++++++++++++++++++++----- revocation/result/results.go | 30 +++++++++++++++--------------- revocation/revocation.go | 7 ++++--- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index b10be551..717ebbfc 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -54,14 +54,29 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR } // check revocation - for _, revokedCert := range crlStore.BaseCRL().RevokedCertificateEntries { - if revokedCert.SerialNumber.Cmp(cert.SerialNumber) == 0 { - return &result.CertRevocationResult{ - Result: result.ResultRevoked, - CRLStatus: result.NewCRLStatus(revokedCert), + var ( + revoked bool + lastRevocationEntry x509.RevocationListEntry + ) + for _, revocationEntry := range crlStore.BaseCRL().RevokedCertificateEntries { + if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { + lastRevocationEntry = revocationEntry + if revocationEntry.ReasonCode == int(result.CRLReasonCodeCertificateHold) { + revoked = true + } else if revocationEntry.ReasonCode == int(result.CRLReasonCodeRemoveFromCRL) { + revoked = false + } else { + revoked = true + break } } } + if revoked { + return &result.CertRevocationResult{ + Result: result.ResultRevoked, + CRLStatus: result.NewCRLStatus(lastRevocationEntry), + } + } return &result.CertRevocationResult{ Result: result.ResultOK, diff --git a/revocation/result/results.go b/revocation/result/results.go index fddd3e3d..4c663177 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -84,25 +84,25 @@ func NewServerResult(result Result, server string, err error) *ServerResult { } } -// ReasonCode is CRL reason code -type ReasonCode int +// CRLReasonCode is CRL reason code +type CRLReasonCode int const ( - Unspecified ReasonCode = iota - KeyCompromise - CACompromise - AffiliationChanged - Superseded - CessationOfOperation - CertificateHold + CRLReasonCodeUnspecified CRLReasonCode = iota + CRLReasonCodeKeyCompromise + CRLReasonCodeCACompromise + CRLReasonCodeAffiliationChanged + CRLReasonCodeSuperseded + CRLReasonCodeCessationOfOperation + CRLReasonCodeCertificateHold // value 7 is not used - RemoveFromCRL ReasonCode = iota + 1 - PrivilegeWithdrawn - AACompromise + CRLReasonCodeRemoveFromCRL CRLReasonCode = iota + 1 + CRLReasonCodePrivilegeWithdrawn + CRLReasonCodeAACompromise ) // String provides a conversion from a ReasonCode to a string -func (r ReasonCode) String() string { +func (r CRLReasonCode) String() string { switch r { case Unspecified: return "Unspecified" @@ -132,7 +132,7 @@ func (r ReasonCode) String() string { // CRLStatus encapsulates the result of a CRL check type CRLStatus struct { // ReasonCode is the reason code for the CRL status - ReasonCode ReasonCode + ReasonCode CRLReasonCode // RevocationTime is the time at which the certificate was revoked RevocationTime time.Time @@ -141,7 +141,7 @@ type CRLStatus struct { // NewCRLStatus creates a CRLStatus object func NewCRLStatus(entity x509.RevocationListEntry) *CRLStatus { return &CRLStatus{ - ReasonCode: ReasonCode(entity.ReasonCode), + ReasonCode: CRLReasonCode(entity.ReasonCode), RevocationTime: entity.RevocationTime, } } diff --git a/revocation/revocation.go b/revocation/revocation.go index c4b50673..8cd8ba71 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -60,6 +60,7 @@ func (m Mode) CanRunCRL() bool { } type Options struct { + HttpClient *http.Client Mode Mode CertChainPurpose ocsp.Purpose CRLCache cache.Cache @@ -102,8 +103,8 @@ func NewTimestamp(httpClient *http.Client) (Revocation, error) { }, nil } -func NewWithOptions(httpClient *http.Client, opts Options) (Revocation, error) { - if httpClient == nil { +func NewWithOptions(opts Options) (Revocation, error) { + if opts.HttpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } @@ -125,7 +126,7 @@ func NewWithOptions(httpClient *http.Client, opts Options) (Revocation, error) { return &revocation{ mode: opts.Mode, - httpClient: httpClient, + httpClient: opts.HttpClient, certChainPurpose: opts.CertChainPurpose, crlCache: opts.CRLCache, }, nil From 687503560303e10324c7a672b27c9ce07fae8cd7 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 12 Jul 2024 17:33:05 +0800 Subject: [PATCH 012/110] fix: update Signed-off-by: Junjie Gao --- revocation/result/results.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/revocation/result/results.go b/revocation/result/results.go index 4c663177..c5d08342 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -104,25 +104,25 @@ const ( // String provides a conversion from a ReasonCode to a string func (r CRLReasonCode) String() string { switch r { - case Unspecified: + case CRLReasonCodeUnspecified: return "Unspecified" - case KeyCompromise: + case CRLReasonCodeKeyCompromise: return "KeyCompromise" - case CACompromise: + case CRLReasonCodeCACompromise: return "CACompromise" - case AffiliationChanged: + case CRLReasonCodeAffiliationChanged: return "AffiliationChanged" - case Superseded: + case CRLReasonCodeSuperseded: return "Superseded" - case CessationOfOperation: + case CRLReasonCodeCessationOfOperation: return "CessationOfOperation" - case CertificateHold: + case CRLReasonCodeCertificateHold: return "CertificateHold" - case RemoveFromCRL: + case CRLReasonCodeRemoveFromCRL: return "RemoveFromCRL" - case PrivilegeWithdrawn: + case CRLReasonCodePrivilegeWithdrawn: return "PrivilegeWithdrawn" - case AACompromise: + case CRLReasonCodeAACompromise: return "AACompromise" default: return fmt.Sprintf("invalid reason code with value: %d", r) From 5bbd8c72c530c56054a187859ea47fc68ec7d3e4 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 15 Jul 2024 14:00:55 +0800 Subject: [PATCH 013/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 33 +++++++++++++++++++++ revocation/revocation.go | 62 ++++++++-------------------------------- 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 717ebbfc..1f026d78 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "sync" "time" "github.com/notaryproject/notation-core-go/revocation/crl/cache" @@ -18,6 +19,38 @@ type Options struct { Cache cache.Cache } +func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { + if opts.Cache == nil { + return nil, errors.New("cache is required") + } + if opts.HTTPClient == nil { + opts.HTTPClient = http.DefaultClient + } + + certResult := make([]*result.CertRevocationResult, len(opts.CertChain)) + + var wg sync.WaitGroup + for i, cert := range opts.CertChain[:len(opts.CertChain)-1] { + wg.Add(1) + go func(i int, cert *x509.Certificate) { + defer wg.Done() + certResult[i] = CertCheckStatus(cert, cert, opts) + }(i, cert) + } + + // Last is root cert, which will never be revoked by OCSP + certResult[len(opts.CertChain)-1] = &result.CertRevocationResult{ + Result: result.ResultNonRevokable, + ServerResults: []*result.ServerResult{{ + Result: result.ResultNonRevokable, + Error: nil, + }}, + } + + wg.Wait() + return certResult, nil +} + func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { if opts.Cache == nil { return &result.CertRevocationResult{Error: errors.New("cache is required")} diff --git a/revocation/revocation.go b/revocation/revocation.go index 8cd8ba71..a7fecb7c 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -37,38 +37,14 @@ type Revocation interface { Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) } -type Mode int - -const ( - // ModeAutoFallback is the default mode that tries OCSP first and falls back - // to CRL if OCSP doesn't exist or fails with unknown status - ModeAutoFallback Mode = iota - - // ModeOCSPOnly is the mode that only uses OCSP for revocation checking - ModeOCSPOnly - - // ModeCRLOnly is the mode that only uses CRL for revocation checking - ModeCRLOnly -) - -func (m Mode) CanRunOCSP() bool { - return m == ModeAutoFallback || m == ModeOCSPOnly -} - -func (m Mode) CanRunCRL() bool { - return m == ModeAutoFallback || m == ModeCRLOnly -} - type Options struct { HttpClient *http.Client - Mode Mode CertChainPurpose ocsp.Purpose CRLCache cache.Cache } // revocation is an internal struct used for revocation checking type revocation struct { - mode Mode httpClient *http.Client certChainPurpose ocsp.Purpose @@ -79,28 +55,21 @@ type revocation struct { // New constructs a revocation object for code signing certificate chain func New(httpClient *http.Client) (Revocation, error) { - if httpClient == nil { - return nil, errors.New("invalid input: a non-nil httpClient must be specified") - } - - return &revocation{ - httpClient: httpClient, - certChainPurpose: ocsp.PurposeCodeSigning, - crlCache: cache.NewDummyCache(), - }, nil + return NewWithOptions(Options{ + HttpClient: httpClient, + CertChainPurpose: ocsp.PurposeCodeSigning, + CRLCache: cache.NewDummyCache(), + }) } // NewTimestamp contructs a revocation object for timestamping certificate // chain func NewTimestamp(httpClient *http.Client) (Revocation, error) { - if httpClient == nil { - return nil, errors.New("invalid input: a non-nil httpClient must be specified") - } - return &revocation{ - httpClient: httpClient, - certChainPurpose: ocsp.PurposeTimestamping, - crlCache: cache.NewDummyCache(), - }, nil + return NewWithOptions(Options{ + HttpClient: httpClient, + CertChainPurpose: ocsp.PurposeTimestamping, + CRLCache: cache.NewDummyCache(), + }) } func NewWithOptions(opts Options) (Revocation, error) { @@ -108,12 +77,6 @@ func NewWithOptions(opts Options) (Revocation, error) { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } - switch opts.Mode { - case ModeAutoFallback, ModeOCSPOnly, ModeCRLOnly: - default: - return nil, errors.New("invalid input: unknown mode") - } - switch opts.CertChainPurpose { case ocsp.PurposeCodeSigning, ocsp.PurposeTimestamping: default: @@ -125,7 +88,6 @@ func NewWithOptions(opts Options) (Revocation, error) { } return &revocation{ - mode: opts.Mode, httpClient: opts.HttpClient, certChainPurpose: opts.CertChainPurpose, crlCache: opts.CRLCache, @@ -161,7 +123,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti var wg sync.WaitGroup for i, cert := range certChain[:len(certChain)-1] { switch { - case r.mode.CanRunOCSP() && ocsp.HasOCSP(cert): + case ocsp.HasOCSP(cert): // do OCSP check for the certificate wg.Add(1) @@ -170,7 +132,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti defer wg.Done() certResults[i] = ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) }(i, cert) - case r.mode.CanRunCRL() && crl.HasCRL(cert): + case crl.HasCRL(cert): // do CRL check for the certificate wg.Add(1) From 39c94d315bbbd23c0ada7dff2fae25f7bd8fe0ef Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 15 Jul 2024 14:25:30 +0800 Subject: [PATCH 014/110] fix: deprecate revocation Mode Signed-off-by: Junjie Gao --- revocation/revocation.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/revocation/revocation.go b/revocation/revocation.go index a7fecb7c..1b555298 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -163,3 +163,27 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti wg.Wait() return certResults, nil } + +type OCSPRevocation interface { + ValidateOCSPOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) +} + +func (r *revocation) ValidateOCSPOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { + return ocsp.CheckStatus(ocsp.Options{ + CertChain: certChain, + SigningTime: signingTime, + CertChainPurpose: r.certChainPurpose, + }) +} + +type CRLRevocation interface { + ValidateCRLOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) +} + +func (r *revocation) ValidateCRLOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { + return crl.CheckStatus(crl.Options{ + CertChain: certChain, + HTTPClient: r.httpClient, + Cache: r.crlCache, + }) +} From c85d4720adc2c02f01883b99fcd35dc1afc6a905 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 15 Jul 2024 16:09:11 +0800 Subject: [PATCH 015/110] fix: update Signed-off-by: Junjie Gao --- revocation/revocation.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/revocation/revocation.go b/revocation/revocation.go index 1b555298..411765ae 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -26,7 +26,6 @@ import ( "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/ocsp" "github.com/notaryproject/notation-core-go/revocation/result" - coreX509 "github.com/notaryproject/notation-core-go/x509" ) // Revocation is an interface that specifies methods used for revocation checking @@ -102,10 +101,6 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} } - if err := coreX509.ValidateCodeSigningCertChain(certChain, nil); err != nil { - return nil, result.InvalidChainError{Err: err} - } - ocspOpts := ocsp.Options{ CertChain: certChain, SigningTime: signingTime, From a8db1553d1361ffe321103cb09e1bc8b1bf6a182 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 15 Jul 2024 16:42:29 +0800 Subject: [PATCH 016/110] fix: update Signed-off-by: Junjie Gao --- revocation/revocation.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/revocation/revocation.go b/revocation/revocation.go index 411765ae..1d5d1dcb 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -18,6 +18,7 @@ package revocation import ( "crypto/x509" "errors" + "fmt" "net/http" "sync" "time" @@ -26,6 +27,7 @@ import ( "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/ocsp" "github.com/notaryproject/notation-core-go/revocation/result" + coreX509 "github.com/notaryproject/notation-core-go/x509" ) // Revocation is an interface that specifies methods used for revocation checking @@ -101,6 +103,23 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} } + // Validate cert chain structure + // Since this is using authentic signing time, signing time may be zero. + // Thus, it is better to pass nil here than fail for a cert's NotBefore + // being after zero time + switch r.certChainPurpose { + case ocsp.PurposeCodeSigning: + if err := coreX509.ValidateCodeSigningCertChain(certChain, nil); err != nil { + return nil, result.InvalidChainError{Err: err} + } + case ocsp.PurposeTimestamping: + if err := coreX509.ValidateTimestampingCertChain(certChain); err != nil { + return nil, result.InvalidChainError{Err: err} + } + default: + return nil, result.InvalidChainError{Err: fmt.Errorf("unknown certificate chain purpose %v", r.certChainPurpose)} + } + ocspOpts := ocsp.Options{ CertChain: certChain, SigningTime: signingTime, From 534d11e287be4687a6bed454151dc4dc8a35abb9 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Tue, 16 Jul 2024 15:32:50 +0800 Subject: [PATCH 017/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/fs.go | 2 ++ revocation/crl/crl.go | 4 ++-- revocation/crl/fetcher.go | 14 ++++++++++++++ revocation/revocation.go | 3 ++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go index 5aba0c32..a28d7890 100644 --- a/revocation/crl/cache/fs.go +++ b/revocation/crl/cache/fs.go @@ -1,6 +1,7 @@ package cache import ( + "fmt" "io" "os" "path/filepath" @@ -76,6 +77,7 @@ func (c *fileSystemWriter) Close() error { } if !c.canceled { + fmt.Println("Renaming", c.tempFilePath, "to", c.filePath) return os.Rename(c.tempFilePath, c.filePath) } return nil diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 1f026d78..d83b8f39 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -34,7 +34,7 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { wg.Add(1) go func(i int, cert *x509.Certificate) { defer wg.Done() - certResult[i] = CertCheckStatus(cert, cert, opts) + certResult[i] = CertCheckStatus(cert, opts.CertChain[i+1], opts) }(i, cert) } @@ -116,7 +116,7 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR } } - return &result.CertRevocationResult{Result: result.ResultNonRevokable, Error: lastError} + return &result.CertRevocationResult{Result: result.ResultUnknown, Error: lastError} } func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 8e261bd1..7659c0fb 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -2,9 +2,11 @@ package crl import ( "crypto/x509" + "fmt" "io" "net/http" "os" + "time" "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) @@ -29,6 +31,7 @@ func NewCachedCRLFetcher(httpClient *http.Client, cache cache.Cache) CRLFetcher } func (c *cachedCRLFetcher) Fetch(url string) (crlStore CRLStore, cached bool, err error) { + startTime := time.Now() // try to get from cache file, err := c.cache.Get(buildTarName(url)) if err != nil { @@ -54,10 +57,16 @@ func (c *cachedCRLFetcher) Fetch(url string) (crlStore CRLStore, cached bool, er return crlStore, false, nil } + // Calculate the duration + duration := time.Since(startTime) + fmt.Printf("The cache request to %s took %s\n", url, duration) + return crlStore, true, nil } func (c *cachedCRLFetcher) download(url string) (CRLStore, error) { + fmt.Println("downloading CRL from", url) + startTime := time.Now() // fetch from remote resp, err := c.httpClient.Get(url) if err != nil { @@ -70,6 +79,11 @@ func (c *cachedCRLFetcher) download(url string) (CRLStore, error) { return nil, err } + // Calculate the duration + duration := time.Since(startTime) + + fmt.Printf("The HTTP request took %s\n", duration) + crl, err := x509.ParseRevocationList(data) if err != nil { return nil, err diff --git a/revocation/revocation.go b/revocation/revocation.go index 1d5d1dcb..e3412c29 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -184,6 +184,7 @@ type OCSPRevocation interface { func (r *revocation) ValidateOCSPOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { return ocsp.CheckStatus(ocsp.Options{ + HTTPClient: r.httpClient, CertChain: certChain, SigningTime: signingTime, CertChainPurpose: r.certChainPurpose, @@ -196,8 +197,8 @@ type CRLRevocation interface { func (r *revocation) ValidateCRLOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { return crl.CheckStatus(crl.Options{ - CertChain: certChain, HTTPClient: r.httpClient, + CertChain: certChain, Cache: r.crlCache, }) } From 50964ba1d0959f5a92d669ccd63c4dc21a0fac30 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Tue, 16 Jul 2024 10:12:45 +0000 Subject: [PATCH 018/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 22 ++++--- revocation/crl/fetcher.go | 50 +++++++--------- revocation/crl/{crlStore.go => store.go} | 73 ++++++++++++++---------- 3 files changed, 75 insertions(+), 70 deletions(-) rename revocation/crl/{crlStore.go => store.go} (69%) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index d83b8f39..7b490319 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -62,28 +62,32 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR return &result.CertRevocationResult{Error: errors.New("certificate does not support CRL")} } - crlFetcher := NewCachedCRLFetcher(opts.HTTPClient, opts.Cache) + crlFetcher := NewCachedFetcher(opts.HTTPClient, opts.Cache) // Check CRL var lastError error for _, crlURL := range cert.CRLDistributionPoints { - crlStore, cached, err := crlFetcher.Fetch(crlURL) + crlStore, err := crlFetcher.Fetch(crlURL) if err != nil { lastError = err continue } - err = validateCRL(crlStore.BaseCRL(), issuer) + // validate CRL + baseCRLStore, ok := crlStore.(BaseCRLStore) + if !ok { + lastError = errors.New("invalid CRL store") + continue + } + err = validateCRL(baseCRLStore.BaseCRL(), issuer) if err != nil { lastError = err continue } - if !cached { - if err := crlStore.Save(); err != nil { - lastError = err - continue - } + if err := crlStore.Save(); err != nil { + lastError = err + continue } // check revocation @@ -91,7 +95,7 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR revoked bool lastRevocationEntry x509.RevocationListEntry ) - for _, revocationEntry := range crlStore.BaseCRL().RevokedCertificateEntries { + for _, revocationEntry := range baseCRLStore.BaseCRL().RevokedCertificateEntries { if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { lastRevocationEntry = revocationEntry if revocationEntry.ReasonCode == int(result.CRLReasonCodeCertificateHold) { diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 7659c0fb..fd285091 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -6,67 +6,62 @@ import ( "io" "net/http" "os" - "time" "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) -// CRLFetcher is an interface to fetch CRL -type CRLFetcher interface { +// Fetcher is an interface to fetch CRL +type Fetcher interface { // Fetch retrieves the CRL with the given URL - Fetch(url string) (crlStore CRLStore, cached bool, err error) + Fetch(url string) (crlStore Store, err error) } -type cachedCRLFetcher struct { +// cachedFetcher is a CRL fetcher with cache +type cachedFetcher struct { httpClient *http.Client cache cache.Cache } -// NewCachedCRLFetcher creates a new CRL fetcher with cache -func NewCachedCRLFetcher(httpClient *http.Client, cache cache.Cache) CRLFetcher { - return &cachedCRLFetcher{ +// NewCachedFetcher creates a new CRL fetcher with cache +func NewCachedFetcher(httpClient *http.Client, cache cache.Cache) Fetcher { + return &cachedFetcher{ httpClient: httpClient, cache: cache, } } -func (c *cachedCRLFetcher) Fetch(url string) (crlStore CRLStore, cached bool, err error) { - startTime := time.Now() +// Fetch retrieves the CRL with the given URL +func (c *cachedFetcher) Fetch(url string) (crlStore Store, err error) { // try to get from cache - file, err := c.cache.Get(buildTarName(url)) + file, err := c.cache.Get(tarStoreName(url)) if err != nil { if os.IsNotExist(err) { // fallback to fetch from remote crlStore, err := c.download(url) if err != nil { - return nil, false, err + return nil, err } - return crlStore, false, nil + return crlStore, nil } - return nil, false, err + return nil, err } defer file.Close() - crlStore, err = ParseCRLTar(file) + crlStore, err = ParseTarStore(file) if err != nil { crlStore, err := c.download(url) if err != nil { - return nil, false, err + return nil, err } - return crlStore, false, nil + return crlStore, nil } - // Calculate the duration - duration := time.Since(startTime) - fmt.Printf("The cache request to %s took %s\n", url, duration) - - return crlStore, true, nil + return crlStore, nil } -func (c *cachedCRLFetcher) download(url string) (CRLStore, error) { +func (c *cachedFetcher) download(url string) (Store, error) { fmt.Println("downloading CRL from", url) - startTime := time.Now() // fetch from remote resp, err := c.httpClient.Get(url) if err != nil { @@ -79,16 +74,11 @@ func (c *cachedCRLFetcher) download(url string) (CRLStore, error) { return nil, err } - // Calculate the duration - duration := time.Since(startTime) - - fmt.Printf("The HTTP request took %s\n", duration) - crl, err := x509.ParseRevocationList(data) if err != nil { return nil, err } - crlStore := NewCRLTarStore(crl, url, c.cache) + crlStore := NewTarStore(crl, url, c.cache) return crlStore, nil } diff --git a/revocation/crl/crlStore.go b/revocation/crl/store.go similarity index 69% rename from revocation/crl/crlStore.go rename to revocation/crl/store.go index 1f7fd0a5..21da8bd0 100644 --- a/revocation/crl/crlStore.go +++ b/revocation/crl/store.go @@ -14,33 +14,48 @@ import ( "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) -const BaseCRL = "base.crl" -const Metadata = "metadata.json" +const ( + // BaseCRL is the file name of the base CRL + BaseCRL = "base.crl" -type CRLStore interface { - BaseCRL() *x509.RevocationList - Metadata() map[string]string - SetBaseCRL(baseCRL *x509.RevocationList, url string) + // Metadata is the file name of the metadata + Metadata = "metadata.json" +) + +// Store is an interface to store CRL +type Store interface { + // Save saves the CRL Save() error } -type crlTarStore struct { +type BaseCRLStore interface { + // BaseCRL returns the base CRL + BaseCRL() *x509.RevocationList +} + +// tarStore is a CRL store with tarball format +// +// The tarball contains: +// base.crl: the base CRL +// metadata.json: the metadata +type tarStore struct { baseCRL *x509.RevocationList metadata map[string]string cache cache.Cache } -func NewCRLTarStore(baseCRL *x509.RevocationList, url string, cache cache.Cache) CRLStore { - return &crlTarStore{ +// NewTarStore creates a new CRL store with tarball format +func NewTarStore(baseCRL *x509.RevocationList, url string, cache cache.Cache) Store { + return &tarStore{ baseCRL: baseCRL, metadata: map[string]string{BaseCRL: url}, cache: cache} } -// ParseCRLTar parses the CRL tarball -func ParseCRLTar(data io.Reader) (*crlTarStore, error) { - CRLTar := &crlTarStore{} +// ParseTarStore parses the CRL tarball +func ParseTarStore(data io.Reader) (*tarStore, error) { + CRLTar := &tarStore{} // parse the tarball tar := tar.NewReader(data) @@ -98,31 +113,22 @@ func ParseCRLTar(data io.Reader) (*crlTarStore, error) { return CRLTar, nil } -func (c *crlTarStore) BaseCRL() *x509.RevocationList { +func (c *tarStore) BaseCRL() *x509.RevocationList { return c.baseCRL } -func (c *crlTarStore) Metadata() map[string]string { - return c.metadata -} - -func (c *crlTarStore) SetBaseCRL(baseCRL *x509.RevocationList, url string) { - c.baseCRL = baseCRL - - if c.metadata == nil { - c.metadata = make(map[string]string) - } - c.metadata[BaseCRL] = url -} - -func (c *crlTarStore) Save() (err error) { - baseCRLURL, ok := c.metadata[BaseCRL] +func (c *tarStore) Save() (err error) { + baseURL, ok := c.metadata[BaseCRL] if !ok { return errors.New("base.crl URL is missing") } + if c.isCached(baseURL) { + return nil + } + // create cache file - w, err := c.cache.Set(buildTarName(baseCRLURL)) + w, err := c.cache.Set(tarStoreName(baseURL)) if err != nil { return err } @@ -140,7 +146,7 @@ func (c *crlTarStore) Save() (err error) { return nil } -func (c *crlTarStore) saveTar(w cache.WriteCanceler) error { +func (c *tarStore) saveTar(w cache.WriteCanceler) error { tarWriter := tar.NewWriter(w) // Add base.crl if err := addToTar(BaseCRL, c.baseCRL.Raw, tarWriter); err != nil { @@ -155,7 +161,12 @@ func (c *crlTarStore) saveTar(w cache.WriteCanceler) error { return addToTar(Metadata, metadataBytes, tarWriter) } -func buildTarName(url string) string { +func (c *tarStore) isCached(url string) bool { + _, err := c.cache.Get(tarStoreName(url)) + return err == nil +} + +func tarStoreName(url string) string { return hashURL(url) + ".tar" } From cf593b1ef8ac9cdac617e999c3553c2f7cf5f83d Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 19 Jul 2024 03:18:55 +0000 Subject: [PATCH 019/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/cache.go | 9 +++++++++ revocation/crl/cache/dummy.go | 16 ++++++++++++---- revocation/crl/cache/fs.go | 21 +++++++++++++++++++++ revocation/crl/crl.go | 3 +++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index 71db39b1..8b6a7524 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -4,11 +4,20 @@ import ( "io" ) +// Cache is an interface to store the content type Cache interface { + // Get retrieves the content with the given key + // + // if the key does not exist, return os.ErrNotExist Get(key string) (io.ReadCloser, error) + // Set stores the content with the given key Set(key string) (WriteCanceler, error) + // List returns the list of keys + List() ([]string, error) + + // Delete removes the content with the given key Delete(key string) error } diff --git a/revocation/crl/cache/dummy.go b/revocation/crl/cache/dummy.go index f3e1f240..bb04a3ab 100644 --- a/revocation/crl/cache/dummy.go +++ b/revocation/crl/cache/dummy.go @@ -14,32 +14,40 @@ func NewDummyCache() Cache { return &dummyCache{} } -// Get retrieves the CRL from the store +// Get always returns os.ErrNotExist func (d *dummyCache) Get(fileName string) (io.ReadCloser, error) { return nil, os.ErrNotExist } -// Set stores the CRL in the store +// Set returns a dummyWriter func (d *dummyCache) Set(filename string) (WriteCanceler, error) { return &dummyWriter{}, nil } +// List returns empty list +func (d *dummyCache) List() ([]string, error) { + return nil, nil +} + +// Delete does nothing func (d *dummyCache) Delete(fileName string) error { return nil } -// dummyWriter is a WriteCanceler implementation that writes to -// a bytes.Buffer +// dummyWriter is a dummy writer implementation that does nothing type dummyWriter struct { } +// Write does nothing func (d *dummyWriter) Write(p []byte) (int, error) { return len(p), nil } +// Cancel does nothing func (d *dummyWriter) Cancel() { } +// Close does nothing func (d *dummyWriter) Close() error { return nil } diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go index a28d7890..26d93bbc 100644 --- a/revocation/crl/cache/fs.go +++ b/revocation/crl/cache/fs.go @@ -32,6 +32,22 @@ func (f *fileSystemCache) Set(filename string) (WriteCanceler, error) { return newFileSystemWriter(filepath.Join(f.dir, filename)) } +// List returns the list of CRLs in the store +func (f *fileSystemCache) List() ([]string, error) { + files, err := os.ReadDir(f.dir) + if err != nil { + return nil, err + } + + var fileNames []string + for _, file := range files { + fileNames = append(fileNames, file.Name()) + } + + return fileNames, nil +} + +// Delete removes the CRL from the store func (f *fileSystemCache) Delete(fileName string) error { return os.Remove(filepath.Join(f.dir, fileName)) } @@ -77,6 +93,11 @@ func (c *fileSystemWriter) Close() error { } if !c.canceled { + // make directory + if err := os.MkdirAll(filepath.Dir(c.filePath), 0755); err != nil { + return err + } + fmt.Println("Renaming", c.tempFilePath, "to", c.filePath) return os.Rename(c.tempFilePath, c.filePath) } diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 7b490319..02653bc5 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -19,6 +19,9 @@ type Options struct { Cache cache.Cache } +// CheckStatus checks the revocation status of the certificate chain. +// +// It caches the CRL and check the revocation status of the certificate chain. func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { if opts.Cache == nil { return nil, errors.New("cache is required") From c1728456ebe4aac1b7c26a44b23fb825f3dbb1f6 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 19 Jul 2024 03:49:06 +0000 Subject: [PATCH 020/110] fix: update Signed-off-by: Junjie Gao --- revocation/result/errors.go | 13 +++++++++++++ revocation/revocation.go | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/revocation/result/errors.go b/revocation/result/errors.go index 93641998..62e9d8f6 100644 --- a/revocation/result/errors.go +++ b/revocation/result/errors.go @@ -31,3 +31,16 @@ func (e InvalidChainError) Error() string { } return msg } + +type OCSPFallbackError struct { + OCSPErr error + CRLErr error +} + +func (e OCSPFallbackError) Error() string { + msg := "the OCSP check result is of unknown status; fallback to CRL" + if e.OCSPErr != nil { + msg += fmt.Sprintf("; OCSP error: %v", e.OCSPErr) + } + return msg +} diff --git a/revocation/revocation.go b/revocation/revocation.go index e3412c29..5b3b8eff 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -144,7 +144,20 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti // Assume cert chain is accurate and next cert in chain is the issuer go func(i int, cert *x509.Certificate) { defer wg.Done() - certResults[i] = ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) + ocspResult := ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) + + // try CRL check if OCSP is unknown + if crl.HasCRL(cert) && ocspResult != nil && ocspResult.Result == result.ResultUnknown { + crlResult := crl.CertCheckStatus(cert, certChain[i+1], crlOpts) + crlResult.Error = result.OCSPFallbackError{ + OCSPErr: ocspResult.Error, + CRLErr: crlResult.Error, + } + certResults[i] = crlResult + } else { + certResults[i] = ocspResult + } + }(i, cert) case crl.HasCRL(cert): // do CRL check for the certificate From 1aa53fd574604883afa3e6c2e3861a17d6dc8228 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 19 Jul 2024 03:58:21 +0000 Subject: [PATCH 021/110] fix: update Signed-off-by: Junjie Gao --- revocation/revocation.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/revocation/revocation.go b/revocation/revocation.go index 5b3b8eff..cacbfa88 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -146,8 +146,8 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti defer wg.Done() ocspResult := ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) - // try CRL check if OCSP is unknown - if crl.HasCRL(cert) && ocspResult != nil && ocspResult.Result == result.ResultUnknown { + // try CRL check if OCSP result is unknown + if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.HasCRL(cert) { crlResult := crl.CertCheckStatus(cert, certChain[i+1], crlOpts) crlResult.Error = result.OCSPFallbackError{ OCSPErr: ocspResult.Error, @@ -157,7 +157,6 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti } else { certResults[i] = ocspResult } - }(i, cert) case crl.HasCRL(cert): // do CRL check for the certificate From 2d39c3547b095e35f63a3cff6086537f22e0bcd2 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 19 Jul 2024 08:09:06 +0000 Subject: [PATCH 022/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/fs.go | 37 +++++++++++++++++++++++++++++-------- revocation/crl/crl_test.go | 19 ++++++++++++++++--- revocation/crl/fetcher.go | 2 +- revocation/crl/store.go | 36 ++++++++++++++++++++---------------- 4 files changed, 66 insertions(+), 28 deletions(-) diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go index 26d93bbc..9c7f013a 100644 --- a/revocation/crl/cache/fs.go +++ b/revocation/crl/cache/fs.go @@ -5,25 +5,51 @@ import ( "io" "os" "path/filepath" + "time" ) -const tempFileName = "notation-*" +const ( + // tempFileName is the prefix of the temporary file + tempFileName = "notation-*" + + // defaultTTL is the default time to live for the cache + defaultTTL = 24 * 7 * time.Hour +) // fileSystemCache builds on top of OS file system to leverage the file system // concurrency control and atomicity type fileSystemCache struct { dir string + ttl time.Duration } // NewFileSystemCache creates a new file system store -func NewFileSystemCache(dir string) Cache { +func NewFileSystemCache(dir string, ttl time.Duration) (Cache, error) { + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + if ttl == 0 { + ttl = defaultTTL + } + return &fileSystemCache{ dir: dir, - } + ttl: ttl, + }, nil } // Get retrieves the CRL from the store func (f *fileSystemCache) Get(fileName string) (io.ReadCloser, error) { + fileInfo, err := os.Stat(filepath.Join(f.dir, fileName)) + if err != nil { + return nil, err + } + + // check if the file is expired + if time.Since(fileInfo.ModTime()) > f.ttl { + return nil, os.ErrNotExist + } return os.Open(filepath.Join(f.dir, fileName)) } @@ -93,11 +119,6 @@ func (c *fileSystemWriter) Close() error { } if !c.canceled { - // make directory - if err := os.MkdirAll(filepath.Dir(c.filePath), 0755); err != nil { - return err - } - fmt.Println("Renaming", c.tempFilePath, "to", c.filePath) return os.Rename(c.tempFilePath, c.filePath) } diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 1eedd847..9ba33b6b 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/result" @@ -26,10 +27,14 @@ func TestValidCert(t *testing.T) { } certChain := []*x509.Certificate{intermediateCert, rootCert} + cache, err := cache.NewFileSystemCache(tempDir, time.Hour) + if err != nil { + t.Fatal(err) + } opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache.NewFileSystemCache(tempDir), + Cache: cache, } t.Run("validate without cache", func(t *testing.T) { @@ -68,10 +73,14 @@ func TestRevoked(t *testing.T) { } certChain := []*x509.Certificate{intermediateCert, rootCert} + cache, err := cache.NewFileSystemCache(tempDir, time.Hour) + if err != nil { + t.Fatal(err) + } opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache.NewFileSystemCache(tempDir), + Cache: cache, } r := CertCheckStatus(intermediateCert, rootCert, opts) @@ -98,10 +107,14 @@ func TestMSCert(t *testing.T) { } certChain := []*x509.Certificate{intermediateCert, rootCert} + cache, err := cache.NewFileSystemCache(tempDir, time.Hour) + if err != nil { + t.Fatal(err) + } opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache.NewFileSystemCache(tempDir), + Cache: cache, } r := CertCheckStatus(intermediateCert, rootCert, opts) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index fd285091..f99229ea 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -32,6 +32,7 @@ func NewCachedFetcher(httpClient *http.Client, cache cache.Cache) Fetcher { // Fetch retrieves the CRL with the given URL func (c *cachedFetcher) Fetch(url string) (crlStore Store, err error) { + fmt.Println("fetching CRL from", url) // try to get from cache file, err := c.cache.Get(tarStoreName(url)) if err != nil { @@ -61,7 +62,6 @@ func (c *cachedFetcher) Fetch(url string) (crlStore Store, err error) { } func (c *cachedFetcher) download(url string) (Store, error) { - fmt.Println("downloading CRL from", url) // fetch from remote resp, err := c.httpClient.Get(url) if err != nil { diff --git a/revocation/crl/store.go b/revocation/crl/store.go index 21da8bd0..eb8ce133 100644 --- a/revocation/crl/store.go +++ b/revocation/crl/store.go @@ -40,17 +40,29 @@ type BaseCRLStore interface { // metadata.json: the metadata type tarStore struct { baseCRL *x509.RevocationList - metadata map[string]string + metadata metadata cache cache.Cache } +type metadata struct { + BaseCRL crlInfo `json:"base.crl"` +} + +type crlInfo struct { + URL string `json:"url"` +} + // NewTarStore creates a new CRL store with tarball format func NewTarStore(baseCRL *x509.RevocationList, url string, cache cache.Cache) Store { return &tarStore{ - baseCRL: baseCRL, - metadata: map[string]string{BaseCRL: url}, - cache: cache} + baseCRL: baseCRL, + metadata: metadata{ + BaseCRL: crlInfo{ + URL: url, + }, + }, + cache: cache} } // ParseTarStore parses the CRL tarball @@ -86,7 +98,7 @@ func ParseTarStore(data io.Reader) (*tarStore, error) { CRLTar.baseCRL = baseCRL case Metadata: // parse metadata - metadata := make(map[string]string) + var metadata metadata if err := json.NewDecoder(tar).Decode(&metadata); err != nil { return nil, err } @@ -102,12 +114,8 @@ func ParseTarStore(data io.Reader) (*tarStore, error) { return nil, errors.New("base.crl is missing") } - if CRLTar.metadata == nil { - return nil, errors.New("metadata.json is missing") - } - - if _, ok := CRLTar.metadata[BaseCRL]; !ok { - return nil, errors.New("base.crl URL is missing") + if CRLTar.metadata.BaseCRL.URL == "" { + return nil, errors.New("base CRL's URL is missing from metadata.json") } return CRLTar, nil @@ -118,11 +126,7 @@ func (c *tarStore) BaseCRL() *x509.RevocationList { } func (c *tarStore) Save() (err error) { - baseURL, ok := c.metadata[BaseCRL] - if !ok { - return errors.New("base.crl URL is missing") - } - + baseURL := c.metadata.BaseCRL.URL if c.isCached(baseURL) { return nil } From 4c01b9ed9ddbd0efe95c69faeb5aa5caae5e7ef1 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 19 Jul 2024 09:02:10 +0000 Subject: [PATCH 023/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 2 +- revocation/crl/store.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index f99229ea..1499847f 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -49,7 +49,7 @@ func (c *cachedFetcher) Fetch(url string) (crlStore Store, err error) { } defer file.Close() - crlStore, err = ParseTarStore(file) + crlStore, err = ParseTarStore(file, c.cache) if err != nil { crlStore, err := c.download(url) if err != nil { diff --git a/revocation/crl/store.go b/revocation/crl/store.go index eb8ce133..179c3dd9 100644 --- a/revocation/crl/store.go +++ b/revocation/crl/store.go @@ -66,8 +66,13 @@ func NewTarStore(baseCRL *x509.RevocationList, url string, cache cache.Cache) St } // ParseTarStore parses the CRL tarball -func ParseTarStore(data io.Reader) (*tarStore, error) { - CRLTar := &tarStore{} +func ParseTarStore(data io.Reader, cache cache.Cache) (*tarStore, error) { + if cache == nil { + return nil, errors.New("cache is required") + } + CRLTar := &tarStore{ + cache: cache, + } // parse the tarball tar := tar.NewReader(data) From 5787e75cedf099a60ba1ef7689311fb7bf9f8935 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 22 Jul 2024 03:04:07 +0000 Subject: [PATCH 024/110] fix: remove cache Signed-off-by: Junjie Gao --- revocation/crl/cache/cache.go | 27 ----- revocation/crl/cache/dummy.go | 53 --------- revocation/crl/cache/fs.go | 126 --------------------- revocation/crl/crl.go | 77 ++++--------- revocation/crl/crl_test.go | 22 ---- revocation/crl/fetcher.go | 84 -------------- revocation/crl/store.go | 200 ---------------------------------- revocation/revocation.go | 74 ++----------- 8 files changed, 30 insertions(+), 633 deletions(-) delete mode 100644 revocation/crl/cache/cache.go delete mode 100644 revocation/crl/cache/dummy.go delete mode 100644 revocation/crl/cache/fs.go delete mode 100644 revocation/crl/fetcher.go delete mode 100644 revocation/crl/store.go diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go deleted file mode 100644 index 8b6a7524..00000000 --- a/revocation/crl/cache/cache.go +++ /dev/null @@ -1,27 +0,0 @@ -package cache - -import ( - "io" -) - -// Cache is an interface to store the content -type Cache interface { - // Get retrieves the content with the given key - // - // if the key does not exist, return os.ErrNotExist - Get(key string) (io.ReadCloser, error) - - // Set stores the content with the given key - Set(key string) (WriteCanceler, error) - - // List returns the list of keys - List() ([]string, error) - - // Delete removes the content with the given key - Delete(key string) error -} - -type WriteCanceler interface { - io.WriteCloser - Cancel() -} diff --git a/revocation/crl/cache/dummy.go b/revocation/crl/cache/dummy.go deleted file mode 100644 index bb04a3ab..00000000 --- a/revocation/crl/cache/dummy.go +++ /dev/null @@ -1,53 +0,0 @@ -package cache - -import ( - "io" - "os" -) - -// dummyCache is a dummy cache implementation that does nothing -type dummyCache struct { -} - -// NewDummyCache creates a new dummy cache -func NewDummyCache() Cache { - return &dummyCache{} -} - -// Get always returns os.ErrNotExist -func (d *dummyCache) Get(fileName string) (io.ReadCloser, error) { - return nil, os.ErrNotExist -} - -// Set returns a dummyWriter -func (d *dummyCache) Set(filename string) (WriteCanceler, error) { - return &dummyWriter{}, nil -} - -// List returns empty list -func (d *dummyCache) List() ([]string, error) { - return nil, nil -} - -// Delete does nothing -func (d *dummyCache) Delete(fileName string) error { - return nil -} - -// dummyWriter is a dummy writer implementation that does nothing -type dummyWriter struct { -} - -// Write does nothing -func (d *dummyWriter) Write(p []byte) (int, error) { - return len(p), nil -} - -// Cancel does nothing -func (d *dummyWriter) Cancel() { -} - -// Close does nothing -func (d *dummyWriter) Close() error { - return nil -} diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go deleted file mode 100644 index 9c7f013a..00000000 --- a/revocation/crl/cache/fs.go +++ /dev/null @@ -1,126 +0,0 @@ -package cache - -import ( - "fmt" - "io" - "os" - "path/filepath" - "time" -) - -const ( - // tempFileName is the prefix of the temporary file - tempFileName = "notation-*" - - // defaultTTL is the default time to live for the cache - defaultTTL = 24 * 7 * time.Hour -) - -// fileSystemCache builds on top of OS file system to leverage the file system -// concurrency control and atomicity -type fileSystemCache struct { - dir string - ttl time.Duration -} - -// NewFileSystemCache creates a new file system store -func NewFileSystemCache(dir string, ttl time.Duration) (Cache, error) { - if err := os.MkdirAll(dir, 0755); err != nil { - return nil, err - } - - if ttl == 0 { - ttl = defaultTTL - } - - return &fileSystemCache{ - dir: dir, - ttl: ttl, - }, nil -} - -// Get retrieves the CRL from the store -func (f *fileSystemCache) Get(fileName string) (io.ReadCloser, error) { - fileInfo, err := os.Stat(filepath.Join(f.dir, fileName)) - if err != nil { - return nil, err - } - - // check if the file is expired - if time.Since(fileInfo.ModTime()) > f.ttl { - return nil, os.ErrNotExist - } - return os.Open(filepath.Join(f.dir, fileName)) -} - -// Set stores the CRL in the store -func (f *fileSystemCache) Set(filename string) (WriteCanceler, error) { - return newFileSystemWriter(filepath.Join(f.dir, filename)) -} - -// List returns the list of CRLs in the store -func (f *fileSystemCache) List() ([]string, error) { - files, err := os.ReadDir(f.dir) - if err != nil { - return nil, err - } - - var fileNames []string - for _, file := range files { - fileNames = append(fileNames, file.Name()) - } - - return fileNames, nil -} - -// Delete removes the CRL from the store -func (f *fileSystemCache) Delete(fileName string) error { - return os.Remove(filepath.Join(f.dir, fileName)) -} - -// fileSystemWriter is a WriteCanceler implementation that writes to -// a file system file and renames it to the final path when Close is called -type fileSystemWriter struct { - io.WriteCloser - tempFilePath string - filePath string - canceled bool -} - -func newFileSystemWriter(filePath string) (WriteCanceler, error) { - tempFile, err := os.CreateTemp("", tempFileName) - if err != nil { - return nil, err - } - - filePath, err = filepath.Abs(filePath) - if err != nil { - return nil, err - } - - return &fileSystemWriter{ - WriteCloser: tempFile, - tempFilePath: tempFile.Name(), - filePath: filePath, - }, nil -} - -func (c *fileSystemWriter) Write(p []byte) (int, error) { - return c.WriteCloser.Write(p) -} - -func (c *fileSystemWriter) Cancel() { - c.canceled = true -} - -func (c *fileSystemWriter) Close() error { - if err := c.WriteCloser.Close(); err != nil { - return err - } - - if !c.canceled { - fmt.Println("Renaming", c.tempFilePath, "to", c.filePath) - return os.Rename(c.tempFilePath, c.filePath) - } - return nil -} diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 02653bc5..689db34c 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -4,11 +4,10 @@ import ( "crypto/x509" "errors" "fmt" + "io" "net/http" - "sync" "time" - "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/result" ) @@ -16,48 +15,9 @@ import ( type Options struct { CertChain []*x509.Certificate HTTPClient *http.Client - Cache cache.Cache -} - -// CheckStatus checks the revocation status of the certificate chain. -// -// It caches the CRL and check the revocation status of the certificate chain. -func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { - if opts.Cache == nil { - return nil, errors.New("cache is required") - } - if opts.HTTPClient == nil { - opts.HTTPClient = http.DefaultClient - } - - certResult := make([]*result.CertRevocationResult, len(opts.CertChain)) - - var wg sync.WaitGroup - for i, cert := range opts.CertChain[:len(opts.CertChain)-1] { - wg.Add(1) - go func(i int, cert *x509.Certificate) { - defer wg.Done() - certResult[i] = CertCheckStatus(cert, opts.CertChain[i+1], opts) - }(i, cert) - } - - // Last is root cert, which will never be revoked by OCSP - certResult[len(opts.CertChain)-1] = &result.CertRevocationResult{ - Result: result.ResultNonRevokable, - ServerResults: []*result.ServerResult{{ - Result: result.ResultNonRevokable, - Error: nil, - }}, - } - - wg.Wait() - return certResult, nil } func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { - if opts.Cache == nil { - return &result.CertRevocationResult{Error: errors.New("cache is required")} - } if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } @@ -65,40 +25,27 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR return &result.CertRevocationResult{Error: errors.New("certificate does not support CRL")} } - crlFetcher := NewCachedFetcher(opts.HTTPClient, opts.Cache) - // Check CRL var lastError error for _, crlURL := range cert.CRLDistributionPoints { - crlStore, err := crlFetcher.Fetch(crlURL) + baseCRL, err := download(crlURL, opts.HTTPClient) if err != nil { lastError = err continue } - // validate CRL - baseCRLStore, ok := crlStore.(BaseCRLStore) - if !ok { - lastError = errors.New("invalid CRL store") - continue - } - err = validateCRL(baseCRLStore.BaseCRL(), issuer) + err = validateCRL(baseCRL, issuer) if err != nil { lastError = err continue } - if err := crlStore.Save(); err != nil { - lastError = err - continue - } - // check revocation var ( revoked bool lastRevocationEntry x509.RevocationListEntry ) - for _, revocationEntry := range baseCRLStore.BaseCRL().RevokedCertificateEntries { + for _, revocationEntry := range baseCRL.RevokedCertificateEntries { if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { lastRevocationEntry = revocationEntry if revocationEntry.ReasonCode == int(result.CRLReasonCodeCertificateHold) { @@ -151,3 +98,19 @@ func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { func HasCRL(cert *x509.Certificate) bool { return len(cert.CRLDistributionPoints) > 0 } + +func download(url string, httpClient *http.Client) (*x509.RevocationList, error) { + // fetch from remote + resp, err := httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return x509.ParseRevocationList(data) +} diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 9ba33b6b..0c76053b 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -6,15 +6,11 @@ import ( "os" "path/filepath" "testing" - "time" - "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/result" ) func TestValidCert(t *testing.T) { - tempDir := t.TempDir() - // read intermediate cert file intermediateCert, err := loadCertFile(filepath.Join("testdata", "valid", "valid.cer")) if err != nil { @@ -27,14 +23,9 @@ func TestValidCert(t *testing.T) { } certChain := []*x509.Certificate{intermediateCert, rootCert} - cache, err := cache.NewFileSystemCache(tempDir, time.Hour) - if err != nil { - t.Fatal(err) - } opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache, } t.Run("validate without cache", func(t *testing.T) { @@ -59,8 +50,6 @@ func TestValidCert(t *testing.T) { } func TestRevoked(t *testing.T) { - tempDir := t.TempDir() - // read intermediate cert file intermediateCert, err := loadCertFile(filepath.Join("testdata", "revoked", "revoked.cer")) if err != nil { @@ -73,14 +62,9 @@ func TestRevoked(t *testing.T) { } certChain := []*x509.Certificate{intermediateCert, rootCert} - cache, err := cache.NewFileSystemCache(tempDir, time.Hour) - if err != nil { - t.Fatal(err) - } opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache, } r := CertCheckStatus(intermediateCert, rootCert, opts) @@ -93,7 +77,6 @@ func TestRevoked(t *testing.T) { } func TestMSCert(t *testing.T) { - tempDir := t.TempDir() // read intermediate cert file intermediateCert, err := loadCertFile(filepath.Join("testdata", "ms", "msleaf.cer")) @@ -107,14 +90,9 @@ func TestMSCert(t *testing.T) { } certChain := []*x509.Certificate{intermediateCert, rootCert} - cache, err := cache.NewFileSystemCache(tempDir, time.Hour) - if err != nil { - t.Fatal(err) - } opts := Options{ CertChain: certChain, HTTPClient: http.DefaultClient, - Cache: cache, } r := CertCheckStatus(intermediateCert, rootCert, opts) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go deleted file mode 100644 index 1499847f..00000000 --- a/revocation/crl/fetcher.go +++ /dev/null @@ -1,84 +0,0 @@ -package crl - -import ( - "crypto/x509" - "fmt" - "io" - "net/http" - "os" - - "github.com/notaryproject/notation-core-go/revocation/crl/cache" -) - -// Fetcher is an interface to fetch CRL -type Fetcher interface { - // Fetch retrieves the CRL with the given URL - Fetch(url string) (crlStore Store, err error) -} - -// cachedFetcher is a CRL fetcher with cache -type cachedFetcher struct { - httpClient *http.Client - cache cache.Cache -} - -// NewCachedFetcher creates a new CRL fetcher with cache -func NewCachedFetcher(httpClient *http.Client, cache cache.Cache) Fetcher { - return &cachedFetcher{ - httpClient: httpClient, - cache: cache, - } -} - -// Fetch retrieves the CRL with the given URL -func (c *cachedFetcher) Fetch(url string) (crlStore Store, err error) { - fmt.Println("fetching CRL from", url) - // try to get from cache - file, err := c.cache.Get(tarStoreName(url)) - if err != nil { - if os.IsNotExist(err) { - // fallback to fetch from remote - crlStore, err := c.download(url) - if err != nil { - return nil, err - } - return crlStore, nil - } - - return nil, err - } - defer file.Close() - - crlStore, err = ParseTarStore(file, c.cache) - if err != nil { - crlStore, err := c.download(url) - if err != nil { - return nil, err - } - return crlStore, nil - } - - return crlStore, nil -} - -func (c *cachedFetcher) download(url string) (Store, error) { - // fetch from remote - resp, err := c.httpClient.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - crl, err := x509.ParseRevocationList(data) - if err != nil { - return nil, err - } - - crlStore := NewTarStore(crl, url, c.cache) - return crlStore, nil -} diff --git a/revocation/crl/store.go b/revocation/crl/store.go deleted file mode 100644 index 179c3dd9..00000000 --- a/revocation/crl/store.go +++ /dev/null @@ -1,200 +0,0 @@ -package crl - -import ( - "archive/tar" - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "time" - - "github.com/notaryproject/notation-core-go/revocation/crl/cache" -) - -const ( - // BaseCRL is the file name of the base CRL - BaseCRL = "base.crl" - - // Metadata is the file name of the metadata - Metadata = "metadata.json" -) - -// Store is an interface to store CRL -type Store interface { - // Save saves the CRL - Save() error -} - -type BaseCRLStore interface { - // BaseCRL returns the base CRL - BaseCRL() *x509.RevocationList -} - -// tarStore is a CRL store with tarball format -// -// The tarball contains: -// base.crl: the base CRL -// metadata.json: the metadata -type tarStore struct { - baseCRL *x509.RevocationList - metadata metadata - - cache cache.Cache -} - -type metadata struct { - BaseCRL crlInfo `json:"base.crl"` -} - -type crlInfo struct { - URL string `json:"url"` -} - -// NewTarStore creates a new CRL store with tarball format -func NewTarStore(baseCRL *x509.RevocationList, url string, cache cache.Cache) Store { - return &tarStore{ - baseCRL: baseCRL, - metadata: metadata{ - BaseCRL: crlInfo{ - URL: url, - }, - }, - cache: cache} -} - -// ParseTarStore parses the CRL tarball -func ParseTarStore(data io.Reader, cache cache.Cache) (*tarStore, error) { - if cache == nil { - return nil, errors.New("cache is required") - } - CRLTar := &tarStore{ - cache: cache, - } - - // parse the tarball - tar := tar.NewReader(data) - - for { - header, err := tar.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - - switch header.Name { - case BaseCRL: - // parse base.crl - data, err := io.ReadAll(tar) - if err != nil { - return nil, err - } - - var baseCRL *x509.RevocationList - baseCRL, err = x509.ParseRevocationList(data) - if err != nil { - return nil, err - } - - CRLTar.baseCRL = baseCRL - case Metadata: - // parse metadata - var metadata metadata - if err := json.NewDecoder(tar).Decode(&metadata); err != nil { - return nil, err - } - - CRLTar.metadata = metadata - - default: - return nil, fmt.Errorf("unknown file in tarball: %s", header.Name) - } - } - - if CRLTar.baseCRL == nil { - return nil, errors.New("base.crl is missing") - } - - if CRLTar.metadata.BaseCRL.URL == "" { - return nil, errors.New("base CRL's URL is missing from metadata.json") - } - - return CRLTar, nil -} - -func (c *tarStore) BaseCRL() *x509.RevocationList { - return c.baseCRL -} - -func (c *tarStore) Save() (err error) { - baseURL := c.metadata.BaseCRL.URL - if c.isCached(baseURL) { - return nil - } - - // create cache file - w, err := c.cache.Set(tarStoreName(baseURL)) - if err != nil { - return err - } - defer func() { - if cerr := w.Close(); cerr != nil && err == nil { - err = cerr - } - }() - - if err := c.saveTar(w); err != nil { - w.Cancel() - return err - } - - return nil -} - -func (c *tarStore) saveTar(w cache.WriteCanceler) error { - tarWriter := tar.NewWriter(w) - // Add base.crl - if err := addToTar(BaseCRL, c.baseCRL.Raw, tarWriter); err != nil { - return err - } - - // Add metadataBytes.json - metadataBytes, err := json.Marshal(c.metadata) - if err != nil { - return err - } - return addToTar(Metadata, metadataBytes, tarWriter) -} - -func (c *tarStore) isCached(url string) bool { - _, err := c.cache.Get(tarStoreName(url)) - return err == nil -} - -func tarStoreName(url string) string { - return hashURL(url) + ".tar" -} - -// hashURL hashes the URL with SHA256 and returns the hex-encoded result -func hashURL(url string) string { - hash := sha256.Sum256([]byte(url)) - return hex.EncodeToString(hash[:]) -} - -func addToTar(fileName string, data []byte, tw *tar.Writer) error { - header := &tar.Header{ - Name: fileName, - Size: int64(len(data)), - Mode: 0644, - ModTime: time.Now(), - } - if err := tw.WriteHeader(header); err != nil { - return err - } - _, err := tw.Write(data) - return err -} diff --git a/revocation/revocation.go b/revocation/revocation.go index cacbfa88..3ad5289b 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -24,7 +24,6 @@ import ( "time" "github.com/notaryproject/notation-core-go/revocation/crl" - "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/ocsp" "github.com/notaryproject/notation-core-go/revocation/result" coreX509 "github.com/notaryproject/notation-core-go/x509" @@ -38,60 +37,33 @@ type Revocation interface { Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) } -type Options struct { - HttpClient *http.Client - CertChainPurpose ocsp.Purpose - CRLCache cache.Cache -} - // revocation is an internal struct used for revocation checking type revocation struct { httpClient *http.Client certChainPurpose ocsp.Purpose - - // crlCache caches the CRL files; the default one is memory cache - crlCache cache.Cache } // New constructs a revocation object for code signing certificate chain func New(httpClient *http.Client) (Revocation, error) { - return NewWithOptions(Options{ - HttpClient: httpClient, - CertChainPurpose: ocsp.PurposeCodeSigning, - CRLCache: cache.NewDummyCache(), - }) + if httpClient == nil { + return nil, errors.New("invalid input: a non-nil httpClient must be specified") + } + return &revocation{ + httpClient: httpClient, + certChainPurpose: ocsp.PurposeCodeSigning, + }, nil } // NewTimestamp contructs a revocation object for timestamping certificate // chain func NewTimestamp(httpClient *http.Client) (Revocation, error) { - return NewWithOptions(Options{ - HttpClient: httpClient, - CertChainPurpose: ocsp.PurposeTimestamping, - CRLCache: cache.NewDummyCache(), - }) -} - -func NewWithOptions(opts Options) (Revocation, error) { - if opts.HttpClient == nil { + if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } - - switch opts.CertChainPurpose { - case ocsp.PurposeCodeSigning, ocsp.PurposeTimestamping: - default: - return nil, errors.New("invalid input: unknown cert chain purpose") - } - - if opts.CRLCache == nil { - opts.CRLCache = cache.NewDummyCache() - } - return &revocation{ - httpClient: opts.HttpClient, - certChainPurpose: opts.CertChainPurpose, - crlCache: opts.CRLCache, + httpClient: httpClient, + certChainPurpose: ocsp.PurposeTimestamping, }, nil } @@ -130,7 +102,6 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti crlOpts := crl.Options{ CertChain: certChain, HTTPClient: r.httpClient, - Cache: r.crlCache, } certResults := make([]*result.CertRevocationResult, len(certChain)) @@ -189,28 +160,3 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti wg.Wait() return certResults, nil } - -type OCSPRevocation interface { - ValidateOCSPOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) -} - -func (r *revocation) ValidateOCSPOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { - return ocsp.CheckStatus(ocsp.Options{ - HTTPClient: r.httpClient, - CertChain: certChain, - SigningTime: signingTime, - CertChainPurpose: r.certChainPurpose, - }) -} - -type CRLRevocation interface { - ValidateCRLOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) -} - -func (r *revocation) ValidateCRLOnly(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { - return crl.CheckStatus(crl.Options{ - HTTPClient: r.httpClient, - CertChain: certChain, - Cache: r.crlCache, - }) -} From 41746fbf20213c6108a36f431b7d1cbb3dac5b6b Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 22 Jul 2024 03:33:03 +0000 Subject: [PATCH 025/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 50 +++++++++++++++++++++++++++++------- revocation/crl/error.go | 1 - revocation/result/results.go | 21 +++++++-------- 3 files changed, 50 insertions(+), 22 deletions(-) delete mode 100644 revocation/crl/error.go diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 689db34c..bdbf8548 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -25,18 +25,24 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR return &result.CertRevocationResult{Error: errors.New("certificate does not support CRL")} } - // Check CRL - var lastError error - for _, crlURL := range cert.CRLDistributionPoints { + // Check CRLs + crlResults := make([]*result.CRLResult, len(cert.CRLDistributionPoints)) + for i, crlURL := range cert.CRLDistributionPoints { baseCRL, err := download(crlURL, opts.HTTPClient) if err != nil { - lastError = err + crlResults[i] = &result.CRLResult{ + URL: crlURL, + Error: err, + } continue } err = validateCRL(baseCRL, issuer) if err != nil { - lastError = err + crlResults[i] = &result.CRLResult{ + URL: crlURL, + Error: err, + } continue } @@ -47,6 +53,14 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR ) for _, revocationEntry := range baseCRL.RevokedCertificateEntries { if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { + if err := validateCRLEntry(revocationEntry); err != nil { + crlResults[i] = &result.CRLResult{ + URL: crlURL, + Error: err, + } + break + } + lastRevocationEntry = revocationEntry if revocationEntry.ReasonCode == int(result.CRLReasonCodeCertificateHold) { revoked = true @@ -60,17 +74,24 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR } if revoked { return &result.CertRevocationResult{ - Result: result.ResultRevoked, - CRLStatus: result.NewCRLStatus(lastRevocationEntry), + Result: result.ResultRevoked, + CRLResults: []*result.CRLResult{{ + URL: crlURL, + ReasonCode: result.CRLReasonCode(lastRevocationEntry.ReasonCode), + RevocationTime: lastRevocationEntry.RevocationTime}}, } } return &result.CertRevocationResult{ - Result: result.ResultOK, + Result: result.ResultOK, + CRLResults: []*result.CRLResult{{URL: crlURL}}, } } - return &result.CertRevocationResult{Result: result.ResultUnknown, Error: lastError} + return &result.CertRevocationResult{ + Result: result.ResultUnknown, + CRLResults: crlResults, + Error: crlResults[len(crlResults)-1].Error} } func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { @@ -94,6 +115,17 @@ func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { return nil } +func validateCRLEntry(entry x509.RevocationListEntry) error { + // ensure all extension are non-critical + for _, ext := range entry.Extensions { + if ext.Critical { + return fmt.Errorf("CRL entry contains unsupported critical extension: %v", ext.Id) + } + } + + return nil +} + // HasCRL checks if the certificate supports CRL. func HasCRL(cert *x509.Certificate) bool { return len(cert.CRLDistributionPoints) > 0 diff --git a/revocation/crl/error.go b/revocation/crl/error.go deleted file mode 100644 index d65b2a07..00000000 --- a/revocation/crl/error.go +++ /dev/null @@ -1 +0,0 @@ -package crl diff --git a/revocation/result/results.go b/revocation/result/results.go index c5d08342..9cbc56e5 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -15,7 +15,6 @@ package result import ( - "crypto/x509" "fmt" "strconv" "time" @@ -129,21 +128,19 @@ func (r CRLReasonCode) String() string { } } -// CRLStatus encapsulates the result of a CRL check -type CRLStatus struct { +// CRLResult encapsulates the result of a CRL check +type CRLResult struct { + // URL is the URL of the CRL that was checked + URL string + // ReasonCode is the reason code for the CRL status ReasonCode CRLReasonCode // RevocationTime is the time at which the certificate was revoked RevocationTime time.Time -} -// NewCRLStatus creates a CRLStatus object -func NewCRLStatus(entity x509.RevocationListEntry) *CRLStatus { - return &CRLStatus{ - ReasonCode: CRLReasonCode(entity.ReasonCode), - RevocationTime: entity.RevocationTime, - } + // Error is set if there is an error associated with the revocation check + Error error } // CertRevocationResult encapsulates the result for a single certificate in the @@ -168,8 +165,8 @@ type CertRevocationResult struct { // status from being retrieved. These are all contained here for evaluation ServerResults []*ServerResult - // CRLStatus is the result of the CRL check for this certificate - CRLStatus *CRLStatus + // CRLResults is the result of the CRL check for this certificate + CRLResults []*CRLResult // Error is set if there is an error associated with the revocation check Error error From 759b72774a80f7f66a924c809e17aad7f6cad48e Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 22 Jul 2024 08:30:20 +0000 Subject: [PATCH 026/110] fix: refactor Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 158 ++++++++++++++++++++++++------------- revocation/crl/crl_test.go | 9 ++- revocation/revocation.go | 11 ++- 3 files changed, 114 insertions(+), 64 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index bdbf8548..8a31fb43 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -1,26 +1,35 @@ package crl import ( + "context" "crypto/x509" "errors" "fmt" "io" "net/http" + "net/url" "time" "github.com/notaryproject/notation-core-go/revocation/result" ) -// Options specifies values that are needed to check OCSP revocation +// Options specifies values that are needed to check CRL type Options struct { - CertChain []*x509.Certificate - HTTPClient *http.Client + CertChain []*x509.Certificate + HTTPClient *http.Client + SigningTime time.Time } -func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { +// CertCheckStatus checks the revocation status of a certificate using CRL +func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { + if ctx == nil { + ctx = context.Background() + } + if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } + if !HasCRL(cert) { return &result.CertRevocationResult{Error: errors.New("certificate does not support CRL")} } @@ -28,7 +37,7 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR // Check CRLs crlResults := make([]*result.CRLResult, len(cert.CRLDistributionPoints)) for i, crlURL := range cert.CRLDistributionPoints { - baseCRL, err := download(crlURL, opts.HTTPClient) + baseCRL, err := download(ctx, crlURL, opts.HTTPClient) if err != nil { crlResults[i] = &result.CRLResult{ URL: crlURL, @@ -39,53 +48,14 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR err = validateCRL(baseCRL, issuer) if err != nil { - crlResults[i] = &result.CRLResult{ - URL: crlURL, - Error: err, - } - continue - } - - // check revocation - var ( - revoked bool - lastRevocationEntry x509.RevocationListEntry - ) - for _, revocationEntry := range baseCRL.RevokedCertificateEntries { - if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { - if err := validateCRLEntry(revocationEntry); err != nil { - crlResults[i] = &result.CRLResult{ - URL: crlURL, - Error: err, - } - break - } - - lastRevocationEntry = revocationEntry - if revocationEntry.ReasonCode == int(result.CRLReasonCodeCertificateHold) { - revoked = true - } else if revocationEntry.ReasonCode == int(result.CRLReasonCodeRemoveFromCRL) { - revoked = false - } else { - revoked = true - break - } - } - } - if revoked { return &result.CertRevocationResult{ - Result: result.ResultRevoked, - CRLResults: []*result.CRLResult{{ - URL: crlURL, - ReasonCode: result.CRLReasonCode(lastRevocationEntry.ReasonCode), - RevocationTime: lastRevocationEntry.RevocationTime}}, + Result: result.ResultUnknown, + CRLResults: crlResults, + Error: err, } } - return &result.CertRevocationResult{ - Result: result.ResultOK, - CRLResults: []*result.CRLResult{{URL: crlURL}}, - } + return checkRevocation(cert, baseCRL, crlURL, opts.SigningTime) } return &result.CertRevocationResult{ @@ -95,9 +65,9 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR } func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { - // check crl expiration + // after NextUpdate time, new CRL will be issued. (See RFC 5280, Section 5.1.2.5) if time.Now().After(crl.NextUpdate) { - return errors.New("CRL is expired") + return fmt.Errorf("CRL is expired: %v", crl.NextUpdate) } // check signature @@ -105,7 +75,7 @@ func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { return fmt.Errorf("CRL signature verification failed: %v", err) } - // check extensions + // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) for _, ext := range crl.Extensions { if ext.Critical { return fmt.Errorf("CRL contains unsupported critical extension: %v", ext.Id) @@ -115,9 +85,65 @@ func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { return nil } -func validateCRLEntry(entry x509.RevocationListEntry) error { - // ensure all extension are non-critical - for _, ext := range entry.Extensions { +func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, crlURL string, signingTime time.Time) *result.CertRevocationResult { + // check revocation + var ( + revoked bool + lastRevocationEntry x509.RevocationListEntry + ) + for _, revocationEntry := range baseCRL.RevokedCertificateEntries { + if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { + if err := validateRevocationEntry(revocationEntry); err != nil { + return &result.CertRevocationResult{ + Result: result.ResultUnknown, + CRLResults: []*result.CRLResult{ + { + URL: crlURL, + Error: err, + }, + }, + } + } + + // validate revocation time + if !revocationEntry.RevocationTime.IsZero() && signingTime.Before(revocationEntry.RevocationTime) { + // certificate is revoked after signing time, so it is valid + continue + } + lastRevocationEntry = revocationEntry + + if revocationEntry.ReasonCode == int(result.CRLReasonCodeCertificateHold) { + // certificate is revoked but not permanently + revoked = true + } else if revocationEntry.ReasonCode == int(result.CRLReasonCodeRemoveFromCRL) { + // certificate has been removed from the CRL + revoked = false + } else { + // permanently revoked + revoked = true + break + } + } + } + if revoked { + return &result.CertRevocationResult{ + Result: result.ResultRevoked, + CRLResults: []*result.CRLResult{{ + URL: crlURL, + ReasonCode: result.CRLReasonCode(lastRevocationEntry.ReasonCode), + RevocationTime: lastRevocationEntry.RevocationTime}}, + } + } + + return &result.CertRevocationResult{ + Result: result.ResultOK, + CRLResults: []*result.CRLResult{{URL: crlURL}}, + } +} + +func validateRevocationEntry(entry x509.RevocationListEntry) error { + // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) + for _, ext := range entry.ExtraExtensions { if ext.Critical { return fmt.Errorf("CRL entry contains unsupported critical extension: %v", ext.Id) } @@ -131,14 +157,34 @@ func HasCRL(cert *x509.Certificate) bool { return len(cert.CRLDistributionPoints) > 0 } -func download(url string, httpClient *http.Client) (*x509.RevocationList, error) { +func download(ctx context.Context, crlURL string, httpClient *http.Client) (*x509.RevocationList, error) { + // validate URL + parsedURL, err := url.Parse(crlURL) + if err != nil { + return nil, err + } + + if parsedURL.Scheme != "http" { + return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme) + } + // fetch from remote - resp, err := httpClient.Get(url) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) + if err != nil { + return nil, err + + } + + resp, err := httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("failed to download CRL from %s: %s", crlURL, resp.Status) + } + data, err := io.ReadAll(resp.Body) if err != nil { return nil, err diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 0c76053b..e40bfbe4 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -1,6 +1,7 @@ package crl import ( + "context" "crypto/x509" "net/http" "os" @@ -29,7 +30,7 @@ func TestValidCert(t *testing.T) { } t.Run("validate without cache", func(t *testing.T) { - r := CertCheckStatus(intermediateCert, rootCert, opts) + r := CertCheckStatus(context.Background(), intermediateCert, rootCert, opts) if r.Error != nil { t.Fatal(err) } @@ -39,7 +40,7 @@ func TestValidCert(t *testing.T) { }) t.Run("validate with cache", func(t *testing.T) { - r := CertCheckStatus(intermediateCert, rootCert, opts) + r := CertCheckStatus(context.Background(), intermediateCert, rootCert, opts) if r.Error != nil { t.Fatal(err) } @@ -67,7 +68,7 @@ func TestRevoked(t *testing.T) { HTTPClient: http.DefaultClient, } - r := CertCheckStatus(intermediateCert, rootCert, opts) + r := CertCheckStatus(context.Background(), intermediateCert, rootCert, opts) if r.Error != nil { t.Fatal(r.Error) } @@ -95,7 +96,7 @@ func TestMSCert(t *testing.T) { HTTPClient: http.DefaultClient, } - r := CertCheckStatus(intermediateCert, rootCert, opts) + r := CertCheckStatus(context.Background(), intermediateCert, rootCert, opts) if r.Error != nil { t.Fatal(err) } diff --git a/revocation/revocation.go b/revocation/revocation.go index 3ad5289b..f1c77322 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -16,6 +16,7 @@ package revocation import ( + "context" "crypto/x509" "errors" "fmt" @@ -74,6 +75,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti if len(certChain) == 0 { return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} } + ctx := context.Background() // Validate cert chain structure // Since this is using authentic signing time, signing time may be zero. @@ -100,8 +102,9 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti } crlOpts := crl.Options{ - CertChain: certChain, - HTTPClient: r.httpClient, + CertChain: certChain, + HTTPClient: r.httpClient, + SigningTime: signingTime, } certResults := make([]*result.CertRevocationResult, len(certChain)) @@ -119,7 +122,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti // try CRL check if OCSP result is unknown if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.HasCRL(cert) { - crlResult := crl.CertCheckStatus(cert, certChain[i+1], crlOpts) + crlResult := crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts) crlResult.Error = result.OCSPFallbackError{ OCSPErr: ocspResult.Error, CRLErr: crlResult.Error, @@ -136,7 +139,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti go func(i int, cert *x509.Certificate) { defer wg.Done() - certResults[i] = crl.CertCheckStatus(cert, certChain[i+1], crlOpts) + certResults[i] = crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts) }(i, cert) default: certResults[i] = &result.CertRevocationResult{ From 17da50084fdc69303f51adaec9ef45af72906509 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Tue, 23 Jul 2024 07:38:56 +0000 Subject: [PATCH 027/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 21 +-- revocation/crl/crl_test.go | 309 +++++++++++++++++++++++++++++++++- revocation/ocsp/ocsp_test.go | 6 +- revocation/revocation.go | 1 - revocation/revocation_test.go | 6 +- testhelper/certificatetest.go | 29 +++- 6 files changed, 337 insertions(+), 35 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 8a31fb43..90671a2f 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -15,17 +15,12 @@ import ( // Options specifies values that are needed to check CRL type Options struct { - CertChain []*x509.Certificate HTTPClient *http.Client SigningTime time.Time } // CertCheckStatus checks the revocation status of a certificate using CRL func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { - if ctx == nil { - ctx = context.Background() - } - if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } @@ -46,7 +41,7 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts O continue } - err = validateCRL(baseCRL, issuer) + err = validate(baseCRL, issuer) if err != nil { return &result.CertRevocationResult{ Result: result.ResultUnknown, @@ -64,15 +59,15 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts O Error: crlResults[len(crlResults)-1].Error} } -func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { +func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { // after NextUpdate time, new CRL will be issued. (See RFC 5280, Section 5.1.2.5) - if time.Now().After(crl.NextUpdate) { + if !crl.NextUpdate.IsZero() && time.Now().After(crl.NextUpdate) { return fmt.Errorf("CRL is expired: %v", crl.NextUpdate) } // check signature if err := crl.CheckSignatureFrom(issuer); err != nil { - return fmt.Errorf("CRL signature verification failed: %v", err) + return fmt.Errorf("CRL signature verification failed: %w", err) } // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) @@ -85,6 +80,7 @@ func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error { return nil } +// checkRevocation checks if the certificate is revoked or not func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, crlURL string, signingTime time.Time) *result.CertRevocationResult { // check revocation var ( @@ -102,11 +98,12 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, crlUR Error: err, }, }, + Error: err, } } // validate revocation time - if !revocationEntry.RevocationTime.IsZero() && signingTime.Before(revocationEntry.RevocationTime) { + if !signingTime.IsZero() && signingTime.Before(revocationEntry.RevocationTime) { // certificate is revoked after signing time, so it is valid continue } @@ -157,7 +154,7 @@ func HasCRL(cert *x509.Certificate) bool { return len(cert.CRLDistributionPoints) > 0 } -func download(ctx context.Context, crlURL string, httpClient *http.Client) (*x509.RevocationList, error) { +func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { // validate URL parsedURL, err := url.Parse(crlURL) if err != nil { @@ -175,7 +172,7 @@ func download(ctx context.Context, crlURL string, httpClient *http.Client) (*x50 } - resp, err := httpClient.Do(req) + resp, err := client.Do(req) if err != nil { return nil, err } diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index e40bfbe4..d1b863ca 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -3,10 +3,14 @@ package crl import ( "context" "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" "net/http" "os" "path/filepath" "testing" + "time" "github.com/notaryproject/notation-core-go/revocation/result" ) @@ -23,9 +27,7 @@ func TestValidCert(t *testing.T) { t.Fatal(err) } - certChain := []*x509.Certificate{intermediateCert, rootCert} opts := Options{ - CertChain: certChain, HTTPClient: http.DefaultClient, } @@ -62,9 +64,7 @@ func TestRevoked(t *testing.T) { t.Fatal(err) } - certChain := []*x509.Certificate{intermediateCert, rootCert} opts := Options{ - CertChain: certChain, HTTPClient: http.DefaultClient, } @@ -73,7 +73,7 @@ func TestRevoked(t *testing.T) { t.Fatal(r.Error) } if r.Result != result.ResultRevoked { - t.Fatal("unexpected result") + t.Fatalf("unexpected result, got %s", r.Result) } } @@ -90,9 +90,7 @@ func TestMSCert(t *testing.T) { t.Fatal(err) } - certChain := []*x509.Certificate{intermediateCert, rootCert} opts := Options{ - CertChain: certChain, HTTPClient: http.DefaultClient, } @@ -117,3 +115,300 @@ func loadCertFile(certPath string) (*x509.Certificate, error) { } return cert, nil } + +func TestCertCheckStatus(t *testing.T) { + t.Run("http client is nil", func(t *testing.T) { + r := CertCheckStatus(context.Background(), &x509.Certificate{}, &x509.Certificate{}, Options{}) + if r.Error == nil { + t.Fatal("expected error") + } + }) + + t.Run("download error", func(t *testing.T) { + cert := &x509.Certificate{ + CRLDistributionPoints: []string{"http://example.com"}, + } + r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, Options{ + HTTPClient: &http.Client{ + Transport: errorRoundTripperMock{}, + }, + }) + if r.Error == nil { + t.Fatal("expected error") + } + }) +} + +func TestValidate(t *testing.T) { + t.Run("expired CRL", func(t *testing.T) { + crl := &x509.RevocationList{ + NextUpdate: time.Now().Add(-time.Hour), + } + + if err := validate(crl, &x509.Certificate{}); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("check signature failed", func(t *testing.T) { + crl := &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + } + + if err := validate(crl, &x509.Certificate{}); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("unsupported CRL critical extensions", func(t *testing.T) { + crl := &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + } + + cert := &x509.Certificate{} + + if err := validate(crl, cert); err == nil { + t.Fatal("expected error") + } + }) +} + +func TestCheckRevocation(t *testing.T) { + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + } + crlURL := "http://example.com" + signingTime := time.Now() + + t.Run("not revoked", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(2), + }, + }, + } + r := checkRevocation(cert, baseCRL, crlURL, signingTime) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultOK { + t.Fatalf("unexpected result, got %s", r.Result) + } + }) + + t.Run("revoked", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Now().Add(-time.Hour), + }, + }, + } + r := checkRevocation(cert, baseCRL, crlURL, signingTime) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + + t.Run("revoked but signing time is before revocation time", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Now().Add(time.Hour), + }, + }, + } + r := checkRevocation(cert, baseCRL, crlURL, signingTime) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultOK { + t.Fatalf("unexpected result, got %s", r.Result) + } + }) + + t.Run("revoked and signing time is zero", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Time{}, + }, + }, + } + r := checkRevocation(cert, baseCRL, crlURL, time.Time{}) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + + t.Run("revoked but not permanently", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Time{}, + }, + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), + RevocationTime: time.Time{}, + }, + }, + } + r := checkRevocation(cert, baseCRL, crlURL, signingTime) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultOK { + t.Fatalf("unexpected result, got %s", r.Result) + } + }) + + t.Run("revocation entry validation error", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ExtraExtensions: []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: true, + }, + }, + }, + }, + } + r := checkRevocation(cert, baseCRL, crlURL, signingTime) + if r.Error == nil { + t.Fatal("expected error") + } + }) +} + +func TestValidateRevocationEntry(t *testing.T) { + t.Run("invalid extension", func(t *testing.T) { + entry := x509.RevocationListEntry{ + ExtraExtensions: []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: true, + }, + }, + } + if err := validateRevocationEntry(entry); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("valid extension", func(t *testing.T) { + entry := x509.RevocationListEntry{ + ExtraExtensions: []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: false, + }, + }, + } + if err := validateRevocationEntry(entry); err != nil { + t.Fatal(err) + } + }) +} + +func TestDownload(t *testing.T) { + t.Run("parse url error", func(t *testing.T) { + _, err := download(context.Background(), ":", http.DefaultClient) + if err == nil { + t.Fatal("expected error") + } + }) + t.Run("https download", func(t *testing.T) { + _, err := download(context.Background(), "https://example.com", http.DefaultClient) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("http.NewRequestWithContext error", func(t *testing.T) { + var ctx context.Context = nil + _, err := download(ctx, "http://example.com", &http.Client{}) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("client.Do error", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: errorRoundTripperMock{}, + }) + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("status code is not 2xx", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: serverErrorRoundTripperMock{}, + }) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("readAll error", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: readFailedRoundTripperMock{}, + }) + if err == nil { + t.Fatal("expected error") + } + }) +} + +type errorRoundTripperMock struct{} + +func (rt errorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("error") +} + +type serverErrorRoundTripperMock struct{} + +func (rt serverErrorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + }, nil +} + +type readFailedRoundTripperMock struct{} + +func (rt readFailedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: errorReaderMock{}, + }, nil +} + +type errorReaderMock struct{} + +func (r errorReaderMock) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("error") +} + +func (r errorReaderMock) Close() error { + return nil +} diff --git a/revocation/ocsp/ocsp_test.go b/revocation/ocsp/ocsp_test.go index fad22333..d70922bc 100644 --- a/revocation/ocsp/ocsp_test.go +++ b/revocation/ocsp/ocsp_test.go @@ -203,7 +203,7 @@ func TestCheckStatusForNonSelfSignedSingleCert(t *testing.T) { func TestCheckStatusForChain(t *testing.T) { zeroTime := time.Time{} - testChain := testhelper.GetRevokableRSAChain(6) + testChain := testhelper.GetRevokableRSAChain(6, true, false) revokableChain := make([]*x509.Certificate, 6) for i, tuple := range testChain { revokableChain[i] = tuple.Cert @@ -457,7 +457,7 @@ func TestCheckStatusErrors(t *testing.T) { rootCertTuple := testhelper.GetRSARootCertificate() noOCSPChain := []*x509.Certificate{leafCertTuple.Cert, rootCertTuple.Cert} - revokableTuples := testhelper.GetRevokableRSAChain(3) + revokableTuples := testhelper.GetRevokableRSAChain(3, true, false) noRootChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert} backwardsChain := []*x509.Certificate{revokableTuples[2].Cert, revokableTuples[1].Cert, revokableTuples[0].Cert} okChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert, revokableTuples[2].Cert} @@ -663,7 +663,7 @@ func TestCheckStatusErrors(t *testing.T) { } func TestCheckOCSPInvalidChain(t *testing.T) { - revokableTuples := testhelper.GetRevokableRSAChain(4) + revokableTuples := testhelper.GetRevokableRSAChain(4, true, false) misorderedIntermediateTuples := []testhelper.RSACertTuple{revokableTuples[1], revokableTuples[0], revokableTuples[2], revokableTuples[3]} misorderedIntermediateChain := []*x509.Certificate{revokableTuples[1].Cert, revokableTuples[0].Cert, revokableTuples[2].Cert, revokableTuples[3].Cert} for i, cert := range misorderedIntermediateChain { diff --git a/revocation/revocation.go b/revocation/revocation.go index f1c77322..4a3593b5 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -102,7 +102,6 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti } crlOpts := crl.Options{ - CertChain: certChain, HTTPClient: r.httpClient, SigningTime: signingTime, } diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index f9d4f4e5..2419fd16 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -226,7 +226,7 @@ func TestCheckRevocationStatusForRootCert(t *testing.T) { func TestCheckRevocationStatusForChain(t *testing.T) { zeroTime := time.Time{} - testChain := testhelper.GetRevokableRSAChain(6) + testChain := testhelper.GetRevokableRSAChain(6, true, false) revokableChain := make([]*x509.Certificate, 6) for i, tuple := range testChain { revokableChain[i] = tuple.Cert @@ -705,7 +705,7 @@ func TestCheckRevocationErrors(t *testing.T) { rootCertTuple := testhelper.GetRSARootCertificate() noOCSPChain := []*x509.Certificate{leafCertTuple.Cert, rootCertTuple.Cert} - revokableTuples := testhelper.GetRevokableRSAChain(3) + revokableTuples := testhelper.GetRevokableRSAChain(3, true, false) noRootChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert} backwardsChain := []*x509.Certificate{revokableTuples[2].Cert, revokableTuples[1].Cert, revokableTuples[0].Cert} okChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert, revokableTuples[2].Cert} @@ -864,7 +864,7 @@ func TestCheckRevocationErrors(t *testing.T) { } func TestCheckRevocationInvalidChain(t *testing.T) { - revokableTuples := testhelper.GetRevokableRSAChain(4) + revokableTuples := testhelper.GetRevokableRSAChain(4, true, false) misorderedIntermediateTuples := []testhelper.RSACertTuple{revokableTuples[1], revokableTuples[0], revokableTuples[2], revokableTuples[3]} misorderedIntermediateChain := []*x509.Certificate{revokableTuples[1].Cert, revokableTuples[0].Cert, revokableTuples[2].Cert, revokableTuples[3].Cert} for i, cert := range misorderedIntermediateChain { diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index 54a31c0e..99080bd3 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -76,15 +76,15 @@ func GetRevokableRSALeafCertificate() RSACertTuple { } // GetRevokableRSAChain returns a chain of certificates that specify a local OCSP server signed using RSA algorithm -func GetRevokableRSAChain(size int) []RSACertTuple { +func GetRevokableRSAChain(size int, enabledOCSP, enabledCRL bool) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) for i := size - 2; i > 0; i-- { - chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, enabledOCSP, enabledCRL) } if size > 1 { - chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false) + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false, enabledOCSP, enabledCRL) } return chain } @@ -96,10 +96,10 @@ func GetRevokableRSATimestampChain(size int) []RSACertTuple { chain := make([]RSACertTuple, size) chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) for i := size - 2; i > 0; i-- { - chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, true, false) } if size > 1 { - chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true) + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true, true, false) } return chain } @@ -171,12 +171,18 @@ func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } -func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) RSACertTuple { +func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int, enabledOCSP, enabledCRL bool) RSACertTuple { template := getCertTemplate(previous == nil, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign - template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} + if enabledOCSP { + template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} + } + if enabledCRL { + template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d.crl", index)} + + } return getRSACertTupleWithTemplate(template, previous.PrivateKey, previous) } @@ -190,12 +196,17 @@ func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { return getRSACertTupleWithTemplate(template, pk, nil) } -func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp bool) RSACertTuple { +func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp, enabledOCSP, enabledCRL bool) RSACertTuple { template := getCertTemplate(false, codesign, timestamp, cn) template.BasicConstraintsValid = true template.IsCA = false template.KeyUsage = x509.KeyUsageDigitalSignature - template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} + if enabledOCSP { + template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} + } + if enabledCRL { + template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d.crl", index)} + } return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } From 75884a5969714dc90656f16e46124ed9d2d10ff8 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Tue, 23 Jul 2024 08:01:32 +0000 Subject: [PATCH 028/110] fix: complete test for crl package Signed-off-by: Junjie Gao --- revocation/crl/crl_test.go | 64 +++++++++++++++++++++++++++++++++-- testhelper/certificatetest.go | 2 +- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index d1b863ca..ef0021d0 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -1,10 +1,13 @@ package crl import ( + "bytes" "context" + "crypto/rand" "crypto/x509" "crypto/x509/pkix" "fmt" + "io" "math/big" "net/http" "os" @@ -13,6 +16,7 @@ import ( "time" "github.com/notaryproject/notation-core-go/revocation/result" + "github.com/notaryproject/notation-core-go/testhelper" ) func TestValidCert(t *testing.T) { @@ -137,6 +141,20 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal("expected error") } }) + + t.Run("CRL validate failed", func(t *testing.T) { + cert := &x509.Certificate{ + CRLDistributionPoints: []string{"http://example.com"}, + } + r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, Options{ + HTTPClient: &http.Client{ + Transport: expiredCRLRoundTripperMock{}, + }, + }) + if r.Error == nil { + t.Fatal("expected error") + } + }) } func TestValidate(t *testing.T) { @@ -161,13 +179,32 @@ func TestValidate(t *testing.T) { }) t.Run("unsupported CRL critical extensions", func(t *testing.T) { - crl := &x509.RevocationList{ + chain := testhelper.GetRevokableRSAChain(1, false, true) + issuerCert := chain[0].Cert + issuerKey := chain[0].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) } - cert := &x509.Certificate{} + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatal(err) + } + + // add unsupported critical extension + crl.Extensions = []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: true, + }, + } - if err := validate(crl, cert); err == nil { + if err := validate(crl, issuerCert); err == nil { t.Fatal("expected error") } }) @@ -403,6 +440,27 @@ func (rt readFailedRoundTripperMock) RoundTrip(req *http.Request) (*http.Respons }, nil } +type expiredCRLRoundTripperMock struct{} + +func (rt expiredCRLRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + chain := testhelper.GetRevokableRSAChain(1, false, true) + issuerCert := chain[0].Cert + issuerKey := chain[0].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(-time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(crlBytes)), + }, nil +} + type errorReaderMock struct{} func (r errorReaderMock) Read(p []byte) (n int, err error) { diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index 99080bd3..ad64a913 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -191,7 +191,7 @@ func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { template := getCertTemplate(true, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true - template.KeyUsage = x509.KeyUsageCertSign + template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign template.MaxPathLen = pathLen return getRSACertTupleWithTemplate(template, pk, nil) } From e92aff7aedfae83e5a400e9a22a2df5e1d73bf55 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Tue, 23 Jul 2024 12:36:22 +0000 Subject: [PATCH 029/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 1 - revocation/revocation_test.go | 242 ++++++++++++++++++++++++++++++++++ testhelper/certificatetest.go | 16 ++- 3 files changed, 252 insertions(+), 7 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 90671a2f..ead2cee8 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -169,7 +169,6 @@ func download(ctx context.Context, crlURL string, client *http.Client) (*x509.Re req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) if err != nil { return nil, err - } resp, err := client.Do(req) diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index 2419fd16..059b7192 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -14,10 +14,17 @@ package revocation import ( + "bytes" + "crypto/rand" "crypto/x509" "errors" "fmt" + "io" + "math/big" "net/http" + "reflect" + "strconv" + "strings" "testing" "time" @@ -58,6 +65,23 @@ func validateEquivalentCertResults(certResults, expectedCertResults []*result.Ce t.Errorf("Expected certResults[%d].ServerResults[%d].Error to be %v, but got %v", i, j, expectedCertResults[i].ServerResults[j].Error, serverResult.Error) } } + + if len(certResult.CRLResults) != len(expectedCertResults[i].CRLResults) { + t.Errorf("Length of certResults[%d].CRLResults (%d) did not match expected length (%d)", i, len(certResult.CRLResults), len(expectedCertResults[i].CRLResults)) + } + + for j, crlResult := range certResult.CRLResults { + if crlResult.ReasonCode != expectedCertResults[i].CRLResults[j].ReasonCode { + t.Errorf("Expected certResults[%d].CRLResults[%d].ReasonCode to be %s, but got %s", i, j, expectedCertResults[i].CRLResults[j].ReasonCode, crlResult.ReasonCode) + } + + resultErrorType := reflect.TypeOf(crlResult.Error) + expectedErrorType := reflect.TypeOf(expectedCertResults[i].CRLResults[j].Error) + if resultErrorType != expectedErrorType { + t.Errorf("Expected certResults[%d].CRLResults[%d].Error to be of type %v, but got %v", i, j, expectedErrorType, resultErrorType) + } + + } } } @@ -70,6 +94,15 @@ func getOKCertResult(server string) *result.CertRevocationResult { } } +func getOKCertResultForCRL() *result.CertRevocationResult { + return &result.CertRevocationResult{ + Result: result.ResultOK, + CRLResults: []*result.CRLResult{ + {}, + }, + } +} + func getRootCertResult() *result.CertRevocationResult { return &result.CertRevocationResult{ Result: result.ResultNonRevokable, @@ -476,6 +509,18 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { revokableChain[i].NotBefore = zeroTime } + t.Run("invalid revocation purpose", func(t *testing.T) { + revocationClient := &revocation{ + httpClient: &http.Client{Timeout: 5 * time.Second}, + certChainPurpose: -1, + } + + _, err := revocationClient.Validate(revokableChain, time.Now()) + if err == nil { + t.Error("Expected Validate to fail with an error, but it succeeded") + } + }) + t.Run("empty chain", func(t *testing.T) { r, err := NewTimestamp(&http.Client{Timeout: 5 * time.Second}) if err != nil { @@ -490,6 +535,21 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { t.Error("Expected certResults to be nil when there is an error") } }) + + t.Run("invalid timestamping chain", func(t *testing.T) { + r, err := NewTimestamp(&http.Client{Timeout: 5 * time.Second}) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + + certChain := testhelper.GetRevokableRSATimestampChain(3) + + _, err = r.Validate([]*x509.Certificate{certChain[0].Cert, certChain[1].Cert}, time.Now()) + if err == nil { + t.Errorf("Expected CheckStatus to fail, but got nil") + } + }) + t.Run("check non-revoked chain", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := NewTimestamp(client) @@ -917,3 +977,185 @@ func TestCheckRevocationInvalidChain(t *testing.T) { } }) } + +func TestCRL(t *testing.T) { + t.Run("CRL check valid", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChain(3, false, true) + + revocationClient := &revocation{ + httpClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: false, + }, + }, + } + + certResults, err := revocationClient.Validate([]*x509.Certificate{ + chain[0].Cert, // leaf + chain[1].Cert, // intermediate + chain[2].Cert, // root + }, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResultForCRL(), + getOKCertResultForCRL(), + getRootCertResult(), + } + + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + + t.Run("CRL check with revoked status", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChain(3, false, true) + + revocationClient := &revocation{ + httpClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: true, + }, + }, + } + + certResults, err := revocationClient.Validate([]*x509.Certificate{ + chain[0].Cert, // leaf + chain[1].Cert, // intermediate + chain[2].Cert, // root + }, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + + expectedCertResults := []*result.CertRevocationResult{ + { + Result: result.ResultRevoked, + CRLResults: []*result.CRLResult{ + { + ReasonCode: result.CRLReasonCodeKeyCompromise, + }, + }, + }, + { + Result: result.ResultRevoked, + CRLResults: []*result.CRLResult{ + { + ReasonCode: result.CRLReasonCodeKeyCompromise, + }, + }, + }, + getRootCertResult(), + } + + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + + t.Run("OCSP fallback to CRL", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChain(3, true, true) + + revocationClient := &revocation{ + httpClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: true, + FailOCSP: true, + }, + }, + } + + certResults, err := revocationClient.Validate([]*x509.Certificate{ + chain[0].Cert, // leaf + chain[1].Cert, // intermediate + chain[2].Cert, // root + }, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + + expectedCertResults := []*result.CertRevocationResult{ + { + Result: result.ResultRevoked, + CRLResults: []*result.CRLResult{ + { + ReasonCode: result.CRLReasonCodeKeyCompromise, + }, + }, + Error: result.OCSPFallbackError{}, + }, + { + Result: result.ResultRevoked, + CRLResults: []*result.CRLResult{ + { + ReasonCode: result.CRLReasonCodeKeyCompromise, + }, + }, + Error: result.OCSPFallbackError{}, + }, + getRootCertResult(), + } + + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) +} + +type crlRoundTripper struct { + CertChain []testhelper.RSACertTuple + Revoked bool + FailOCSP bool +} + +func (rt *crlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + // e.g. ocsp URL: http://example.com/chain_ocsp/0 + // e.g. crl URL: http://example.com/chain_crl/0 + parts := strings.Split(req.URL.Path, "/") + + isOCSP := parts[len(parts)-2] == "chain_ocsp" + // fail OCSP + if rt.FailOCSP && isOCSP { + return nil, errors.New("OCSP failed") + } + + // choose the cert suffix based on suffix of request url + // e.g. http://example.com/chain_crl/0 -> 0 + i, err := strconv.Atoi(parts[len(parts)-1]) + if err != nil { + return nil, err + } + if i >= len(rt.CertChain) { + return nil, errors.New("invalid index") + } + + cert := rt.CertChain[i].Cert + crl := &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + } + + if rt.Revoked { + crl.RevokedCertificateEntries = []x509.RevocationListEntry{ + { + SerialNumber: cert.SerialNumber, + RevocationTime: time.Now().Add(-time.Hour), + ReasonCode: int(result.CRLReasonCodeKeyCompromise), + }, + } + } + + issuerCert := rt.CertChain[i+1].Cert + issuerKey := rt.CertChain[i+1].PrivateKey + crlBytes, err := x509.CreateRevocationList(rand.Reader, crl, issuerCert, issuerKey) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(crlBytes)), + }, nil +} diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index ad64a913..ffb87653 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -79,7 +79,7 @@ func GetRevokableRSALeafCertificate() RSACertTuple { func GetRevokableRSAChain(size int, enabledOCSP, enabledCRL bool) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) - chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, enabledCRL) for i := size - 2; i > 0; i-- { chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, enabledOCSP, enabledCRL) } @@ -94,7 +94,7 @@ func GetRevokableRSAChain(size int, enabledOCSP, enabledCRL bool) []RSACertTuple func GetRevokableRSATimestampChain(size int) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) - chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, false) for i := size - 2; i > 0; i-- { chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, true, false) } @@ -180,18 +180,22 @@ func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int, template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} } if enabledCRL { - template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d.crl", index)} + template.KeyUsage |= x509.KeyUsageCRLSign + template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d", index)} } return getRSACertTupleWithTemplate(template, previous.PrivateKey, previous) } -func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { +func getRevokableRSARootChainCertTuple(cn string, pathLen int, enabledCRL bool) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) template := getCertTemplate(true, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true - template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + template.KeyUsage = x509.KeyUsageCertSign + if enabledCRL { + template.KeyUsage |= x509.KeyUsageCRLSign + } template.MaxPathLen = pathLen return getRSACertTupleWithTemplate(template, pk, nil) } @@ -205,7 +209,7 @@ func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index in template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} } if enabledCRL { - template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d.crl", index)} + template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d", index)} } return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } From 5c5c041674840d97e3b5e4f492e09184fd95ed0c Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 01:58:50 +0000 Subject: [PATCH 030/110] fix: update github action rule for branches Signed-off-by: Junjie Gao --- .github/workflows/build.yml | 4 ++-- .github/workflows/codeql.yml | 4 ++-- .github/workflows/license-checker.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 184a10f0..4778a6b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,9 +15,9 @@ name: Build on: push: - branches: main + branches: * pull_request: - branches: main + branches: * jobs: build: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9a4272b1..90e29061 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,9 +15,9 @@ name: "CodeQL" on: push: - branches: main + branches: * pull_request: - branches: main + branches: * schedule: - cron: '38 15 * * 1' diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index 539c4a5b..a9dcf37f 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -15,9 +15,9 @@ name: License Checker on: push: - branches: main + branches: * pull_request: - branches: main + branches: * permissions: contents: write From efe7708e3461964800865ba82e713522348a8c7d Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 02:02:37 +0000 Subject: [PATCH 031/110] fix: update Signed-off-by: Junjie Gao --- .github/workflows/build.yml | 8 ++++++-- .github/workflows/codeql.yml | 8 ++++++-- .github/workflows/license-checker.yml | 8 ++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4778a6b6..ebdbf627 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,9 +15,13 @@ name: Build on: push: - branches: * + branches: + - main + - crl pull_request: - branches: * + branches: + - main + - crl jobs: build: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 90e29061..db702a3b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,9 +15,13 @@ name: "CodeQL" on: push: - branches: * + branches: + - main + - crl pull_request: - branches: * + branches: + - main + - crl schedule: - cron: '38 15 * * 1' diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index a9dcf37f..cf044ee1 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -15,9 +15,13 @@ name: License Checker on: push: - branches: * + branches: + - main + - crl pull_request: - branches: * + branches: + - main + - crl permissions: contents: write From 28bbf22a3e323a226981c5d3e19b04d03afdb924 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 06:35:29 +0000 Subject: [PATCH 032/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 113 ++++++++++++++++++--------- revocation/crl/crl_test.go | 126 +++++++++++++++++++++++++++--- revocation/ocsp/ocsp.go | 12 +-- revocation/result/errors.go | 5 ++ revocation/result/errors_test.go | 38 +++++++++ revocation/result/results.go | 8 +- revocation/result/results_test.go | 35 +++++++++ revocation/revocation.go | 15 ++-- 8 files changed, 289 insertions(+), 63 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index ead2cee8..7057f1e4 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -1,3 +1,18 @@ +// 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 crl provides methods for checking the revocation status of a +// certificate using CRL package crl import ( @@ -8,6 +23,8 @@ import ( "io" "net/http" "net/url" + "sort" + "strings" "time" "github.com/notaryproject/notation-core-go/revocation/result" @@ -25,7 +42,7 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts O opts.HTTPClient = http.DefaultClient } - if !HasCRL(cert) { + if !SupportCRL(cert) { return &result.CertRevocationResult{Error: errors.New("certificate does not support CRL")} } @@ -35,7 +52,6 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts O baseCRL, err := download(ctx, crlURL, opts.HTTPClient) if err != nil { crlResults[i] = &result.CRLResult{ - URL: crlURL, Error: err, } continue @@ -50,7 +66,7 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts O } } - return checkRevocation(cert, baseCRL, crlURL, opts.SigningTime) + return checkRevocation(cert, baseCRL, opts.SigningTime) } return &result.CertRevocationResult{ @@ -59,6 +75,11 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts O Error: crlResults[len(crlResults)-1].Error} } +// SupportCRL checks if the certificate supports CRL. +func SupportCRL(cert *x509.Certificate) bool { + return len(cert.CRLDistributionPoints) > 0 +} + func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { // after NextUpdate time, new CRL will be issued. (See RFC 5280, Section 5.1.2.5) if !crl.NextUpdate.IsZero() && time.Now().After(crl.NextUpdate) { @@ -81,12 +102,23 @@ func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { } // checkRevocation checks if the certificate is revoked or not -func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, crlURL string, signingTime time.Time) *result.CertRevocationResult { - // check revocation - var ( - revoked bool - lastRevocationEntry x509.RevocationListEntry - ) +func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signingTime time.Time) *result.CertRevocationResult { + if cert == nil { + return &result.CertRevocationResult{Error: errors.New("certificate is nil")} + } + + if baseCRL == nil { + return &result.CertRevocationResult{Error: errors.New("CRL is nil")} + } + + // tempRevokedEntries contains revocation entries with reasons such as + // CertificateHold or RemoveFromCRL. + // + // If the certificate is revoked with CertificateHold, it is temporarily + // revoked. If the certificate is shown in the CRL with RemoveFromCRL, + // its revocation is no longer valid. + var tempRevokedEntries []x509.RevocationListEntry + for _, revocationEntry := range baseCRL.RevokedCertificateEntries { if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { if err := validateRevocationEntry(revocationEntry); err != nil { @@ -94,7 +126,6 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, crlUR Result: result.ResultUnknown, CRLResults: []*result.CRLResult{ { - URL: crlURL, Error: err, }, }, @@ -104,37 +135,47 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, crlUR // validate revocation time if !signingTime.IsZero() && signingTime.Before(revocationEntry.RevocationTime) { - // certificate is revoked after signing time, so it is valid continue } - lastRevocationEntry = revocationEntry - - if revocationEntry.ReasonCode == int(result.CRLReasonCodeCertificateHold) { - // certificate is revoked but not permanently - revoked = true - } else if revocationEntry.ReasonCode == int(result.CRLReasonCodeRemoveFromCRL) { - // certificate has been removed from the CRL - revoked = false + + if result.CRLReasonCodeCertificateHold.Equal(revocationEntry.ReasonCode) || + result.CRLReasonCodeRemoveFromCRL.Equal(revocationEntry.ReasonCode) { + // temporarily revoked + tempRevokedEntries = append(tempRevokedEntries, revocationEntry) } else { // permanently revoked - revoked = true - break + return &result.CertRevocationResult{ + Result: result.ResultRevoked, + CRLResults: []*result.CRLResult{{ + ReasonCode: result.CRLReasonCode(revocationEntry.ReasonCode), + RevocationTime: revocationEntry.RevocationTime}}, + } } } } - if revoked { - return &result.CertRevocationResult{ - Result: result.ResultRevoked, - CRLResults: []*result.CRLResult{{ - URL: crlURL, - ReasonCode: result.CRLReasonCode(lastRevocationEntry.ReasonCode), - RevocationTime: lastRevocationEntry.RevocationTime}}, + + // check if the revocation with CertificateHold or RemoveFromCRL + if len(tempRevokedEntries) > 0 { + // sort by revocation time (ascending order) + sort.Slice(tempRevokedEntries, func(i, j int) bool { + return tempRevokedEntries[i].RevocationTime.Before(tempRevokedEntries[j].RevocationTime) + }) + + // the revocation status depends on the most recent one + lastEntry := tempRevokedEntries[len(tempRevokedEntries)-1] + if !result.CRLReasonCodeRemoveFromCRL.Equal(lastEntry.ReasonCode) { + return &result.CertRevocationResult{ + Result: result.ResultRevoked, + CRLResults: []*result.CRLResult{{ + ReasonCode: result.CRLReasonCode(lastEntry.ReasonCode), + RevocationTime: lastEntry.RevocationTime}}, + } } } return &result.CertRevocationResult{ Result: result.ResultOK, - CRLResults: []*result.CRLResult{{URL: crlURL}}, + CRLResults: []*result.CRLResult{}, } } @@ -149,42 +190,36 @@ func validateRevocationEntry(entry x509.RevocationListEntry) error { return nil } -// HasCRL checks if the certificate supports CRL. -func HasCRL(cert *x509.Certificate) bool { - return len(cert.CRLDistributionPoints) > 0 -} - func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { // validate URL parsedURL, err := url.Parse(crlURL) if err != nil { return nil, err } - - if parsedURL.Scheme != "http" { + if strings.ToLower(parsedURL.Scheme) != "http" { return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme) } - // fetch from remote + // download CRL req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) if err != nil { return nil, err } - resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() + // check response if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("failed to download CRL from %s: %s", crlURL, resp.Status) } + // parse CRL data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } - return x509.ParseRevocationList(data) } diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index ef0021d0..0d012a00 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -1,3 +1,16 @@ +// 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 crl import ( @@ -214,9 +227,22 @@ func TestCheckRevocation(t *testing.T) { cert := &x509.Certificate{ SerialNumber: big.NewInt(1), } - crlURL := "http://example.com" signingTime := time.Now() + t.Run("certificate is nil", func(t *testing.T) { + r := checkRevocation(nil, &x509.RevocationList{}, signingTime) + if r.Error == nil { + t.Fatal("expected error") + } + }) + + t.Run("CRL is nil", func(t *testing.T) { + r := checkRevocation(cert, nil, signingTime) + if r.Error == nil { + t.Fatal("expected error") + } + }) + t.Run("not revoked", func(t *testing.T) { baseCRL := &x509.RevocationList{ RevokedCertificateEntries: []x509.RevocationListEntry{ @@ -225,7 +251,7 @@ func TestCheckRevocation(t *testing.T) { }, }, } - r := checkRevocation(cert, baseCRL, crlURL, signingTime) + r := checkRevocation(cert, baseCRL, signingTime) if r.Error != nil { t.Fatal(r.Error) } @@ -244,7 +270,7 @@ func TestCheckRevocation(t *testing.T) { }, }, } - r := checkRevocation(cert, baseCRL, crlURL, signingTime) + r := checkRevocation(cert, baseCRL, signingTime) if r.Error != nil { t.Fatal(r.Error) } @@ -263,7 +289,7 @@ func TestCheckRevocation(t *testing.T) { }, }, } - r := checkRevocation(cert, baseCRL, crlURL, signingTime) + r := checkRevocation(cert, baseCRL, signingTime) if r.Error != nil { t.Fatal(r.Error) } @@ -282,7 +308,7 @@ func TestCheckRevocation(t *testing.T) { }, }, } - r := checkRevocation(cert, baseCRL, crlURL, time.Time{}) + r := checkRevocation(cert, baseCRL, time.Time{}) if r.Error != nil { t.Fatal(r.Error) } @@ -297,16 +323,16 @@ func TestCheckRevocation(t *testing.T) { { SerialNumber: big.NewInt(1), ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Time{}, + RevocationTime: time.Now().Add(-time.Hour), }, { SerialNumber: big.NewInt(1), ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), - RevocationTime: time.Time{}, + RevocationTime: time.Now().Add(-time.Minute * 10), }, }, } - r := checkRevocation(cert, baseCRL, crlURL, signingTime) + r := checkRevocation(cert, baseCRL, signingTime) if r.Error != nil { t.Fatal(r.Error) } @@ -315,6 +341,88 @@ func TestCheckRevocation(t *testing.T) { } }) + t.Run("revoked but not permanently with disordered entry list", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), + RevocationTime: time.Now().Add(-time.Minute * 10), + }, + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Now().Add(-time.Hour), + }, + }, + } + r := checkRevocation(cert, baseCRL, signingTime) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultOK { + t.Fatalf("unexpected result, got %s", r.Result) + } + }) + + t.Run("RemoveFromCRL before CertificateHold", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), + RevocationTime: time.Now().Add(-time.Hour), + }, + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Now().Add(-time.Minute * 10), + }, + }, + } + r := checkRevocation(cert, baseCRL, signingTime) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + + t.Run("multiple CertificateHold with RemoveFromCRL and disordered entry list", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Now().Add(-time.Minute * 20), + }, + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), + RevocationTime: time.Now().Add(-time.Hour), + }, + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Now().Add(-time.Minute * 50), + }, + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), + RevocationTime: time.Now().Add(-time.Minute * 40), + }, + }, + } + r := checkRevocation(cert, baseCRL, signingTime) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + t.Run("revocation entry validation error", func(t *testing.T) { baseCRL := &x509.RevocationList{ RevokedCertificateEntries: []x509.RevocationListEntry{ @@ -329,7 +437,7 @@ func TestCheckRevocation(t *testing.T) { }, }, } - r := checkRevocation(cert, baseCRL, crlURL, signingTime) + r := checkRevocation(cert, baseCRL, signingTime) if r.Error == nil { t.Fatal("expected error") } diff --git a/revocation/ocsp/ocsp.go b/revocation/ocsp/ocsp.go index 55e39bfa..2afd0f80 100644 --- a/revocation/ocsp/ocsp.go +++ b/revocation/ocsp/ocsp.go @@ -116,7 +116,7 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { } func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { - if !HasOCSP(cert) { + if !SupportOCSP(cert) { // OCSP not enabled for this certificate. return &result.CertRevocationResult{ Result: result.ResultNonRevokable, @@ -141,6 +141,11 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR return serverResultsToCertRevocationResult(serverResults) } +// SupportOCSP returns true if the certificate supports OCSP. +func SupportOCSP(cert *x509.Certificate) bool { + return len(cert.OCSPServer) > 0 +} + func checkStatusFromServer(cert, issuer *x509.Certificate, server string, opts Options) *result.ServerResult { // Check valid server if serverURL, err := url.Parse(server); err != nil || !strings.EqualFold(serverURL.Scheme, "http") { @@ -293,8 +298,3 @@ func serverResultsToCertRevocationResult(serverResults []*result.ServerResult) * ServerResults: serverResults, } } - -// HasOCSP returns true if the certificate supports OCSP. -func HasOCSP(cert *x509.Certificate) bool { - return len(cert.OCSPServer) > 0 -} diff --git a/revocation/result/errors.go b/revocation/result/errors.go index 62e9d8f6..cf7bc866 100644 --- a/revocation/result/errors.go +++ b/revocation/result/errors.go @@ -42,5 +42,10 @@ func (e OCSPFallbackError) Error() string { if e.OCSPErr != nil { msg += fmt.Sprintf("; OCSP error: %v", e.OCSPErr) } + + if e.CRLErr != nil { + msg += fmt.Sprintf("; CRL error: %v", e.CRLErr) + } + return msg } diff --git a/revocation/result/errors_test.go b/revocation/result/errors_test.go index 59b47c7b..a406c111 100644 --- a/revocation/result/errors_test.go +++ b/revocation/result/errors_test.go @@ -37,3 +37,41 @@ func TestInvalidChainError(t *testing.T) { } }) } + +func TestOCSPFallbackError(t *testing.T) { + t.Run("without_inner_error", func(t *testing.T) { + err := &OCSPFallbackError{} + expectedMsg := "the OCSP check result is of unknown status; fallback to CRL" + + if err.Error() != expectedMsg { + t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) + } + }) + + t.Run("with_ocsp_error", func(t *testing.T) { + err := &OCSPFallbackError{OCSPErr: errors.New("ocsp error")} + expectedMsg := "the OCSP check result is of unknown status; fallback to CRL; OCSP error: ocsp error" + + if err.Error() != expectedMsg { + t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) + } + }) + + t.Run("with_crl_error", func(t *testing.T) { + err := &OCSPFallbackError{CRLErr: errors.New("crl error")} + expectedMsg := "the OCSP check result is of unknown status; fallback to CRL; CRL error: crl error" + + if err.Error() != expectedMsg { + t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) + } + }) + + t.Run("with_both_errors", func(t *testing.T) { + err := &OCSPFallbackError{OCSPErr: errors.New("ocsp error"), CRLErr: errors.New("crl error")} + expectedMsg := "the OCSP check result is of unknown status; fallback to CRL; OCSP error: ocsp error; CRL error: crl error" + + if err.Error() != expectedMsg { + t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) + } + }) +} diff --git a/revocation/result/results.go b/revocation/result/results.go index 9cbc56e5..61d5bd75 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -100,6 +100,11 @@ const ( CRLReasonCodeAACompromise ) +// Equal checks if the reason code is equal to the given reason code +func (r CRLReasonCode) Equal(reasonCode int) bool { + return int(r) == reasonCode +} + // String provides a conversion from a ReasonCode to a string func (r CRLReasonCode) String() string { switch r { @@ -130,9 +135,6 @@ func (r CRLReasonCode) String() string { // CRLResult encapsulates the result of a CRL check type CRLResult struct { - // URL is the URL of the CRL that was checked - URL string - // ReasonCode is the reason code for the CRL status ReasonCode CRLReasonCode diff --git a/revocation/result/results_test.go b/revocation/result/results_test.go index 1c5a503a..a18ef2bf 100644 --- a/revocation/result/results_test.go +++ b/revocation/result/results_test.go @@ -63,3 +63,38 @@ func TestNewServerResult(t *testing.T) { t.Errorf("Expected %v but got %v", expectedR.Error, r.Error) } } + +func TestCRLReasonCode(t *testing.T) { + expected := []struct { + code int + reason string + }{ + {0, "Unspecified"}, + {1, "KeyCompromise"}, + {2, "CACompromise"}, + {3, "AffiliationChanged"}, + {4, "Superseded"}, + {5, "CessationOfOperation"}, + {6, "CertificateHold"}, + {8, "RemoveFromCRL"}, + {9, "PrivilegeWithdrawn"}, + {10, "AACompromise"}, + } + + for _, e := range expected { + reasonCode := CRLReasonCode(e.code) + if !reasonCode.Equal(e.code) || reasonCode.String() != e.reason { + t.Errorf("Expected %s but got %s", e.reason, CRLReasonCode(e.code).String()) + } + } +} + +func TestInvalidCRLReasonCode(t *testing.T) { + if CRLReasonCode(7).String() != "invalid reason code with value: 7" { + t.Errorf("Expected %s but got %s", "invalid reason code with value: 7", CRLReasonCode(7).String()) + } + + if CRLReasonCode(11).String() != "invalid reason code with value: 11" { + t.Errorf("Expected %s but got %s", "invalid reason code with value: 11", CRLReasonCode(11).String()) + } +} diff --git a/revocation/revocation.go b/revocation/revocation.go index 4a3593b5..84e3ea3b 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -33,8 +33,9 @@ import ( // Revocation is an interface that specifies methods used for revocation checking type Revocation interface { // Validate checks the revocation status for a certificate chain using OCSP - // and returns an array of CertRevocationResults that contain the results - // and any errors that are encountered during the process + // and CRL if OCSP is not available. It returns an array of + // CertRevocationResults that contain the results and any errors that are + // encountered during the process Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) } @@ -70,7 +71,9 @@ func NewTimestamp(httpClient *http.Client) (Revocation, error) { // Validate checks the revocation status for a certificate chain using OCSP or // CRL and returns an array of CertRevocationResults that contain the results -// and any errors that are encountered during the process +// and any errors that are encountered during the process. +// +// The certificate chain is expected to be in the order of leaf to root. func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { if len(certChain) == 0 { return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} @@ -110,7 +113,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti var wg sync.WaitGroup for i, cert := range certChain[:len(certChain)-1] { switch { - case ocsp.HasOCSP(cert): + case ocsp.SupportOCSP(cert): // do OCSP check for the certificate wg.Add(1) @@ -120,7 +123,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti ocspResult := ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) // try CRL check if OCSP result is unknown - if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.HasCRL(cert) { + if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.SupportCRL(cert) { crlResult := crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts) crlResult.Error = result.OCSPFallbackError{ OCSPErr: ocspResult.Error, @@ -131,7 +134,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti certResults[i] = ocspResult } }(i, cert) - case crl.HasCRL(cert): + case crl.SupportCRL(cert): // do CRL check for the certificate wg.Add(1) From a8f3b4b370d7693df7cd39adddd63af79069f5e4 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 07:35:14 +0000 Subject: [PATCH 033/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 2 +- revocation/crl/crl_test.go | 9 ++++ revocation/revocation.go | 77 +++++++++++++++++++++++++------- revocation/revocation_test.go | 82 +++++++++++++++++++++++++++++------ 4 files changed, 141 insertions(+), 29 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 7057f1e4..f977cebf 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -39,7 +39,7 @@ type Options struct { // CertCheckStatus checks the revocation status of a certificate using CRL func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { if opts.HTTPClient == nil { - opts.HTTPClient = http.DefaultClient + return &result.CertRevocationResult{Error: errors.New("invalid input: a non-nil httpClient must be specified")} } if !SupportCRL(cert) { diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 0d012a00..df44cad1 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -141,6 +141,15 @@ func TestCertCheckStatus(t *testing.T) { } }) + t.Run("certificate does not support CRL", func(t *testing.T) { + r := CertCheckStatus(context.Background(), &x509.Certificate{}, &x509.Certificate{}, Options{ + HTTPClient: http.DefaultClient, + }) + if r.Error == nil { + t.Fatal("expected error") + } + }) + t.Run("download error", func(t *testing.T) { cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, diff --git a/revocation/revocation.go b/revocation/revocation.go index 84e3ea3b..f00bf624 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -39,10 +39,26 @@ type Revocation interface { Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) } +// Options specifies values that are needed to check revocation +type Options struct { + // Ctx is a required context used for the revocation check + Ctx context.Context + + // OCSPHTTPClient is a required HTTP client for OCSP request + OCSPHTTPClient *http.Client + + // CRLHTTPClient is a required HTTP client for CRL request + CRLHTTPClient *http.Client + + // CertChainPurpose is the purpose of the certificate chain + CertChainPurpose ocsp.Purpose +} + // revocation is an internal struct used for revocation checking type revocation struct { - httpClient *http.Client - + ctx context.Context + ocspHTTPClient *http.Client + crlHTTPClient *http.Client certChainPurpose ocsp.Purpose } @@ -51,10 +67,13 @@ func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } - return &revocation{ - httpClient: httpClient, - certChainPurpose: ocsp.PurposeCodeSigning, - }, nil + + return NewWithOptions(&Options{ + Ctx: context.Background(), + OCSPHTTPClient: httpClient, + CRLHTTPClient: httpClient, + CertChainPurpose: ocsp.PurposeCodeSigning, + }) } // NewTimestamp contructs a revocation object for timestamping certificate @@ -63,9 +82,40 @@ func NewTimestamp(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } + + return NewWithOptions(&Options{ + Ctx: context.Background(), + OCSPHTTPClient: httpClient, + CRLHTTPClient: httpClient, + CertChainPurpose: ocsp.PurposeTimestamping, + }) +} + +// NewWithOptions constructs a revocation object with the specified options +func NewWithOptions(opts *Options) (Revocation, error) { + if opts.Ctx == nil { + return nil, errors.New("invalid input: a non-nil context must be specified") + } + + if opts.OCSPHTTPClient == nil { + return nil, errors.New("invalid input: a non-nil OCSPHTTPClient must be specified") + } + + if opts.CRLHTTPClient == nil { + return nil, errors.New("invalid input: a non-nil CRLHTTPClient must be specified") + } + + switch opts.CertChainPurpose { + case ocsp.PurposeCodeSigning, ocsp.PurposeTimestamping: + default: + return nil, fmt.Errorf("unknown certificate chain purpose %v", opts.CertChainPurpose) + } + return &revocation{ - httpClient: httpClient, - certChainPurpose: ocsp.PurposeTimestamping, + ctx: opts.Ctx, + ocspHTTPClient: opts.OCSPHTTPClient, + crlHTTPClient: opts.CRLHTTPClient, + certChainPurpose: opts.CertChainPurpose, }, nil } @@ -78,7 +128,6 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti if len(certChain) == 0 { return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} } - ctx := context.Background() // Validate cert chain structure // Since this is using authentic signing time, signing time may be zero. @@ -101,11 +150,11 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti CertChain: certChain, SigningTime: signingTime, CertChainPurpose: r.certChainPurpose, - HTTPClient: r.httpClient, + HTTPClient: r.ocspHTTPClient, } crlOpts := crl.Options{ - HTTPClient: r.httpClient, + HTTPClient: r.crlHTTPClient, SigningTime: signingTime, } @@ -117,14 +166,13 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti // do OCSP check for the certificate wg.Add(1) - // Assume cert chain is accurate and next cert in chain is the issuer go func(i int, cert *x509.Certificate) { defer wg.Done() ocspResult := ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) // try CRL check if OCSP result is unknown if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.SupportCRL(cert) { - crlResult := crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts) + crlResult := crl.CertCheckStatus(r.ctx, cert, certChain[i+1], crlOpts) crlResult.Error = result.OCSPFallbackError{ OCSPErr: ocspResult.Error, CRLErr: crlResult.Error, @@ -140,8 +188,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti go func(i int, cert *x509.Certificate) { defer wg.Done() - - certResults[i] = crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts) + certResults[i] = crl.CertCheckStatus(r.ctx, cert, certChain[i+1], crlOpts) }(i, cert) default: certResults[i] = &result.CertRevocationResult{ diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index 059b7192..7799e358 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -15,6 +15,7 @@ package revocation import ( "bytes" + "context" "crypto/rand" "crypto/x509" "errors" @@ -96,10 +97,8 @@ func getOKCertResult(server string) *result.CertRevocationResult { func getOKCertResultForCRL() *result.CertRevocationResult { return &result.CertRevocationResult{ - Result: result.ResultOK, - CRLResults: []*result.CRLResult{ - {}, - }, + Result: result.ResultOK, + CRLResults: []*result.CRLResult{}, } } @@ -127,8 +126,8 @@ func TestNew(t *testing.T) { revR, ok := r.(*revocation) if !ok { t.Error("Expected New to create an object matching the internal revocation struct") - } else if revR.httpClient != client { - t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.httpClient) + } else if revR.ocspHTTPClient != client { + t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.ocspHTTPClient) } } @@ -511,7 +510,8 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { t.Run("invalid revocation purpose", func(t *testing.T) { revocationClient := &revocation{ - httpClient: &http.Client{Timeout: 5 * time.Second}, + ctx: context.Background(), + ocspHTTPClient: &http.Client{Timeout: 5 * time.Second}, certChainPurpose: -1, } @@ -982,14 +982,19 @@ func TestCRL(t *testing.T) { t.Run("CRL check valid", func(t *testing.T) { chain := testhelper.GetRevokableRSAChain(3, false, true) - revocationClient := &revocation{ - httpClient: &http.Client{ + revocationClient, err := NewWithOptions(&Options{ + Ctx: context.Background(), + CRLHTTPClient: &http.Client{ Timeout: 5 * time.Second, Transport: &crlRoundTripper{ CertChain: chain, Revoked: false, }, }, + OCSPHTTPClient: &http.Client{}, + }) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := revocationClient.Validate([]*x509.Certificate{ @@ -1013,14 +1018,19 @@ func TestCRL(t *testing.T) { t.Run("CRL check with revoked status", func(t *testing.T) { chain := testhelper.GetRevokableRSAChain(3, false, true) - revocationClient := &revocation{ - httpClient: &http.Client{ + revocationClient, err := NewWithOptions(&Options{ + Ctx: context.Background(), + CRLHTTPClient: &http.Client{ Timeout: 5 * time.Second, Transport: &crlRoundTripper{ CertChain: chain, Revoked: true, }, }, + OCSPHTTPClient: &http.Client{}, + }) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := revocationClient.Validate([]*x509.Certificate{ @@ -1058,8 +1068,9 @@ func TestCRL(t *testing.T) { t.Run("OCSP fallback to CRL", func(t *testing.T) { chain := testhelper.GetRevokableRSAChain(3, true, true) - revocationClient := &revocation{ - httpClient: &http.Client{ + revocationClient, err := NewWithOptions(&Options{ + Ctx: context.Background(), + CRLHTTPClient: &http.Client{ Timeout: 5 * time.Second, Transport: &crlRoundTripper{ CertChain: chain, @@ -1067,6 +1078,10 @@ func TestCRL(t *testing.T) { FailOCSP: true, }, }, + OCSPHTTPClient: &http.Client{}, + }) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := revocationClient.Validate([]*x509.Certificate{ @@ -1104,6 +1119,47 @@ func TestCRL(t *testing.T) { }) } +func TestNewWithOptions(t *testing.T) { + t.Run("nil ctx", func(t *testing.T) { + _, err := NewWithOptions(&Options{}) + if err == nil { + t.Error("Expected NewWithOptions to fail with an error, but it succeeded") + } + }) + + t.Run("nil OCSP HTTP Client", func(t *testing.T) { + _, err := NewWithOptions(&Options{ + Ctx: context.Background(), + }) + if err == nil { + t.Error("Expected NewWithOptions to fail with an error, but it succeeded") + } + }) + + t.Run("nil CRL HTTP Client", func(t *testing.T) { + _, err := NewWithOptions(&Options{ + Ctx: context.Background(), + OCSPHTTPClient: &http.Client{}, + }) + if err == nil { + t.Error("Expected NewWithOptions to fail with an error, but it succeeded") + } + }) + + t.Run("invalid CertChainPurpose", func(t *testing.T) { + _, err := NewWithOptions(&Options{ + Ctx: context.Background(), + OCSPHTTPClient: &http.Client{}, + CRLHTTPClient: &http.Client{}, + CertChainPurpose: -1, + }) + if err == nil { + t.Error("Expected NewWithOptions to fail with an error, but it succeeded") + } + }) + +} + type crlRoundTripper struct { CertChain []testhelper.RSACertTuple Revoked bool From 93ee86376966b2b6dc0223dd7bb44b05646888a1 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 07:51:08 +0000 Subject: [PATCH 034/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl_test.go | 145 +++++------------- revocation/crl/testdata/ms/msintermediate.cer | Bin 1909 -> 0 bytes revocation/crl/testdata/ms/msleaf.cer | Bin 1828 -> 0 bytes revocation/crl/testdata/revoked/revoked.cer | Bin 1197 -> 0 bytes revocation/crl/testdata/revoked/root.cer | Bin 1078 -> 0 bytes revocation/crl/testdata/valid/root.cer | Bin 1391 -> 0 bytes revocation/crl/testdata/valid/valid.cer | Bin 1306 -> 0 bytes 7 files changed, 42 insertions(+), 103 deletions(-) delete mode 100644 revocation/crl/testdata/ms/msintermediate.cer delete mode 100644 revocation/crl/testdata/ms/msleaf.cer delete mode 100644 revocation/crl/testdata/revoked/revoked.cer delete mode 100644 revocation/crl/testdata/revoked/root.cer delete mode 100644 revocation/crl/testdata/valid/root.cer delete mode 100644 revocation/crl/testdata/valid/valid.cer diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index df44cad1..958d8b0d 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -23,8 +23,6 @@ import ( "io" "math/big" "net/http" - "os" - "path/filepath" "testing" "time" @@ -32,107 +30,6 @@ import ( "github.com/notaryproject/notation-core-go/testhelper" ) -func TestValidCert(t *testing.T) { - // read intermediate cert file - intermediateCert, err := loadCertFile(filepath.Join("testdata", "valid", "valid.cer")) - if err != nil { - t.Fatal(err) - } - - rootCert, err := loadCertFile(filepath.Join("testdata", "valid", "root.cer")) - if err != nil { - t.Fatal(err) - } - - opts := Options{ - HTTPClient: http.DefaultClient, - } - - t.Run("validate without cache", func(t *testing.T) { - r := CertCheckStatus(context.Background(), intermediateCert, rootCert, opts) - if r.Error != nil { - t.Fatal(err) - } - if r.Result != result.ResultOK { - t.Fatal("unexpected result") - } - }) - - t.Run("validate with cache", func(t *testing.T) { - r := CertCheckStatus(context.Background(), intermediateCert, rootCert, opts) - if r.Error != nil { - t.Fatal(err) - } - if r.Result != result.ResultOK { - t.Fatal("unexpected result") - } - }) -} - -func TestRevoked(t *testing.T) { - // read intermediate cert file - intermediateCert, err := loadCertFile(filepath.Join("testdata", "revoked", "revoked.cer")) - if err != nil { - t.Fatal(err) - } - - rootCert, err := loadCertFile(filepath.Join("testdata", "revoked", "root.cer")) - if err != nil { - t.Fatal(err) - } - - opts := Options{ - HTTPClient: http.DefaultClient, - } - - r := CertCheckStatus(context.Background(), intermediateCert, rootCert, opts) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultRevoked { - t.Fatalf("unexpected result, got %s", r.Result) - } -} - -func TestMSCert(t *testing.T) { - - // read intermediate cert file - intermediateCert, err := loadCertFile(filepath.Join("testdata", "ms", "msleaf.cer")) - if err != nil { - t.Fatal(err) - } - - rootCert, err := loadCertFile(filepath.Join("testdata", "ms", "msintermediate.cer")) - if err != nil { - t.Fatal(err) - } - - opts := Options{ - HTTPClient: http.DefaultClient, - } - - r := CertCheckStatus(context.Background(), intermediateCert, rootCert, opts) - if r.Error != nil { - t.Fatal(err) - } - if r.Result != result.ResultOK { - t.Fatal("unexpected result") - } -} - -func loadCertFile(certPath string) (*x509.Certificate, error) { - intermediateCert, err := os.ReadFile(certPath) - if err != nil { - return nil, err - } - - cert, err := x509.ParseCertificate(intermediateCert) - if err != nil { - return nil, err - } - return cert, nil -} - func TestCertCheckStatus(t *testing.T) { t.Run("http client is nil", func(t *testing.T) { r := CertCheckStatus(context.Background(), &x509.Certificate{}, &x509.Certificate{}, Options{}) @@ -177,6 +74,37 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal("expected error") } }) + + t.Run("revoked", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChain(2, false, true) + issuerCert := chain[1].Cert + issuerKey := chain[1].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: chain[0].Cert.SerialNumber, + RevocationTime: time.Now().Add(-time.Hour), + ReasonCode: int(result.CRLReasonCodeUnspecified), + }, + }, + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, Options{ + HTTPClient: &http.Client{ + Transport: expectedRoundTripperMock{Body: crlBytes}, + }, + }) + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + + }) } func TestValidate(t *testing.T) { @@ -587,3 +515,14 @@ func (r errorReaderMock) Read(p []byte) (n int, err error) { func (r errorReaderMock) Close() error { return nil } + +type expectedRoundTripperMock struct { + Body []byte +} + +func (rt expectedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(rt.Body)), + }, nil +} diff --git a/revocation/crl/testdata/ms/msintermediate.cer b/revocation/crl/testdata/ms/msintermediate.cer deleted file mode 100644 index 029cdd444e45b8684c9852067ddec1a8a9e59048..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1909 zcmcIkX;f3!8qK{KfB+H+NU#P`qk;lCNhE+85M>YyQ^L?PX-F6nNN%{fQ51+8l$N$Y zc~YNJMI0&46Ht*s5R}5Ii0>&NP?7Qkd`fN6C|0mH&-(0IZ>|1&zs}ivf9E@Uee0YJ z^d^}=PyVEW2tzPT=Me}5+qTj4a`>B|{rDb7z>;FNEPs9eOG^kcClkQV&p775j6#ee zQRr}#m_rA%jlh&njge^5E*QE_NsmXS1}n~0KSN<<~&IDmOZhDx^$kxNlTgQQ@N z>kw3ppb|`upWp&u)QNQG54s3J@SmF-o$ph~o&j>-z|HbJNzi;o4alpjnPE|&zw!OnW#sF>42p$@4 zxZ-WMiT5@>>}JZ#uf>_M!@A(uneX_iy8{+kR2R~_ws5b}7-O4q6{0-9+7m?q=_WRl zZJS@@hYvLSwP(gha6Brgo~pv9JyyL-#5rCPcw6hqX;glWdFr%*-{`(Xc7NZhy1CiB zGdtpRU()6s6wTC(B`qFMSal^lpD7Y|-Fvb-I^^fU`od#Bl@wp}a)~%Fu9jPN@LknK zoOXAohMz8f%DO)EeX*`OvUuP%IclFWZBf*9?)-P2|7d`!gM0T{K8O!< z8BV(R!(q7rEdE1)!#}Sn1X4?E={g)S%CJ3){QF$tqbl=A9u{v zz3D6We^7(6`*Sz$sOvwe<3HgG4a-&ET1SjEk%>pUH77zmu+n)e^j6Pkzt#pao>q?S z&+!rVpG$0+yxXf0tP5@Cc?pZ)`n73pw8y(50*1RA6pUC2r0GaKlGx+486M^_(4I=i zXDzR-sXbD};(NaAi@1stc?%LY0<5I8wD(H*lzD+rLr5oZ! z5ee4)i|fvXlecBG&1RxM7fy*h%5R*_ zuC4V}47;6yi4Xxgs)q)F9_j|9Y#|c)KN?u{frCkNNH)tVMKiM2vPON$ z>Hv4=F4|i*X#z0u$H<&zln+(fCJ+*n67rt^>rQgzM`CyTaTLTGWk*sJ5Q7Mc#UED) z^Ku~w_yQk_DbA3|CNMOImxf{Lm2CEwEnD1_@4u*<6j8F(>2gG^Vf!Ic4O^6~Ml^B^ zLASc4VM;(Zc3(`g0Mz%Lh^D~)zqV~)l7$16ASP%DNrZI-Utn&mw<3}7w8;lnMnH$4 zwBo>hvV9pMkyN2k_;;|#~Non1J(y!urZj~`wLA0@8J}C@@fDIKv6@mHw zf0&k{8UJgVjk9buipaE5OvA=ME1oBKn&2U_(HI}DfbYuXx#2}1_Pu)uB837W2!4+N zobUZtb74#Ibo#5_*qo^T@isNyp@Fij z3zR*77jSshUOg?De4qQWjGWtAFXdG$&PYq~ZyDW(OBo9%##2&PibDqX)zvs;_43^g zROzD$#Pj6Ff-(6}o!Lb80|nCP+^OpIS+9lq7(XiY7B7^GB1f{sT2{=J!9$Zpf*X@* z50VYXqEEj3y2dGL>1|fSnu;d-8f)v>34*0VYxa7cemUh_aJ}B{eB|!6m)=;Ee_>xR z`pjIhvt_6KT6>Z9bAAi+xjPH92giu2og!)K@;53X5#Gg5g_imi9K5E|&(Y(vUy zl1;H1HOh`6vxZAhOi-k(Pt&vAA)&rX&}|!iB;#hp-lDLg2;4*|Lnl3 zUuw^`j};__9lUc}xVbte((Hm9^3&xUmSkvVs8^r(rU>NIoz1>MI^CYe&R45OYHgb? z-|{}riP%-&1UKuCWZN-q3kuaUqt&;a<`$S2^Y7OQr@nbh9bfT^<(|5H$nv{KU+%y6 zaN*-;OVX-IYD?eXpnzX%7Eft3pPUw}# W$+YH7ih_2T8O8hf(qVPVufGEcve|I} diff --git a/revocation/crl/testdata/ms/msleaf.cer b/revocation/crl/testdata/ms/msleaf.cer deleted file mode 100644 index 1789f2293fbc3fa85acda51dd9c7b990a524d4d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1828 zcmcIkc{tQ*9RK}h#>|)@$B3Lwj*#meOzv9e*xee#5FIKr#u}N!%tVO{F)fv+bda*y z4m`4z4qe(r$hnbJ%C#6e6bfa+&TO^y-|j#A&-*-|&*%GokN5e0UXX?%f;1&;Arb*2 z5CjJZg6}6_OW&N*sq2Pd1p=yQ^aphsTX1836+YgCi_l&1MRCLSB@J=*<)Gd4dQLTYh6H zFjeSCg5g&^n9X6?&_xj(K9T0_Nwfzf0E~20?HvHwo=kEf*^?aC$)O{VE1w}qZTer@ zGXo}y_V5IQukCqqSOPXPf*8POeHubYG6!a#!i>1|Xf`W0k{C><6MgAaS5E+d4Iujh z8y^=(C(`G9$Zzv~GcKJah-WidAPr&s?eoJ*5Hby+2FZCGA`JmUz4oEQy}553e5(R2 zf0mX=+ExYZ;ZhD*r|xobYCW=`H$1$fAEo1=ei~8H(-LmFZUB>q{9poxSESddU&CGL zcvrN)pGhf6>|ZNmr|oOrpIbgTBXxUVwv{qK<0f9nxDgSU$RN*6U7j>`@=}Q(k;XG( zYK+B<{Hmxaqe9u7e;ego+pVx2-kDvOmAC^%n+B`jWEzs3a#i&?q$&eqMQ5HSHT1Ao z7iuT9AZcoQ47Psol*;QnSNC(Fg?ZyvQeVuS29M)K2OOwwDLY$8#p11e_@SDz-<@OU zCv~3nB!t(&TP)O6!r~ir0`k}TE+T3r>DUxr3!xMyWVgJ&S6+GEucCZ@-YGS!WW06l z{HE8h{hQ*PjflO=nHRRN?qX|(Vfo;@f)DOY${*Q}_)pk9*&8oe@5tMPLB@<` zcR1LdG%p~BJ z7l2KQfW@dVum%Jd1{KmI)M$~2?`mh4kdR=@`C?mJCXZvs-@@kcg?3Cqtld9`FtZ0= zS6B|@o|T^p*IJM)ZRMdL7LBz+!%9jR7)EUd8^MMz7$79Hf33w5h=l*Wnh2;U#v#D) zJPH9Ipshe=3Zw#XU+N;Ue}7blY6>2OL}0|<+D-o3$?XP1xx0r=aenKN)EqLew65cp zSY$b1R5ycRc>iQJGS4V$uxxx5Cw#-nqcB+HU=FLP~=iDzKNKFz2sh^afY zTl1A9*kY=*lkAw^d`8wtZ144&Ex7UFY+R3QiBUQ2x&>GjA5!clJ6M%>T-&1dv43a( zuO&N^r7?Sce_(yrJEgPHjV_(0pR7J_%dNpU43B(t^*{$EKb={+`90pT)*&&#M{kU< zQ8+OsT$bnPxAy?&FjOj)Ozs}*+qJs3&-uwPq=iwMY@D5_ijDL8F{H({Yv$?ud;<+} z&IL(sA!BTM>{9V7nOYg4YdRw(WS76SPb0=}KreHLm0m>I^~|Odd_cwTjd|ulsdcG5 zw&sWRSFrT!E4}XCLK%|^;>%HG-zlrAx_SKO{BXJRy&#=>(&1{^qsm$uE!7lLen?{=F+=Ju7L2yrKkT)x$%Nb~joUXzqv7?*U=V@TuKF)jw9Tgg diff --git a/revocation/crl/testdata/revoked/revoked.cer b/revocation/crl/testdata/revoked/revoked.cer deleted file mode 100644 index 781deca25aab3239c60c678ee714474fb634d13d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1197 zcmXqLVp(a>#5{2UGZP~dlR!;P?B2gkf>Oo%&i+wn-0{JHmyJ`a&7D}zC` zA-4f18*?ZNn=q5RlcBVMB#6VoBjjF`npl!rq~M#Fmz+nI)+yhKdIAAUS3p2}egq1?SYFlFYQsWT2S} z!Kp=MnaQce26E!O1||lEKnMm=;=D!{hDOFfWNK+@8D-G84#{<)!7vVoFjHuVp}2u4 z*fqj@K-ZKg_#|ehDmZ7BR2s?|$bcOsEE=P4xSr6{=OCT8XsG%+e62QVWm19KB2KLb#li>Zl`kztc%>yl5ks~xSA z+|1XVJ(y6s$1?S&mz4XWoo77GWX-thYbv>OiPO__o{zfpggZ8;Tk2EO#T~ed-pBLK%__K zH4`%<1LNYxF9wYt!Kp)5nMJ}ttU+XfAeXY;w2$m>9v%{|+7EP1uyv?$BZ$oQXy1(?*?4ERCHg+Y8)17;v)AP5pr zVBs?0FkoY20Wuj3TtGtdEVc&L29^uV7nrsgLz6&qQ4VSn&<7@3q%;dlvU)&;2BvHr z+HAnw!_LUaqG_ORpbFy~Ft$lT4a`q2E z%2{1m-j{>;Ue4Ql&_c@aez!@=4vY4M3#YCK{>u}!qxH_ivaQ>CJD*%t*KDZrHe9gU z!9=*+)ao-QN72>1C^x406-&}jFrPo%TXH;lckS$4_Y140+*`>bX!4L#s_6NN&?%Rn zPZsiRaoi)eFIaOcv+Zy93u?ztd~e?3x8a6bM5D1=_!Nr>=InjzwuPJ!ko%$a_|IMQ zSC;)xkAD+SQagKjzSP5|3KQ>#9J_bvQ%-42DYq^ECYx?Ik&274=WghF#PDz^OKvGj zF7V82d;L`MwRakpdu7GdNaclPa&=*(sn?0R=`onMMjRG=L)wC`G0J2iD A&j0`b diff --git a/revocation/crl/testdata/revoked/root.cer b/revocation/crl/testdata/revoked/root.cer deleted file mode 100644 index 5c715faed85eae3569262d7f3dd3fa93a62dff60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1078 zcmXqLVlgskVwPIK%*4pV#K>sC%f_kI=F#?@mywZ`mBFCeklTQhjX9KsO_<5u$xzxr z62#%)5ppj|O)N<*Qt(a8OU_6w1~Lr=4ER8j>^$tji8*QcMJa|-1`;3BO}8;hq+(!9=D%XaqM2>(L9Krh@_kR3etljY8_*?Y$UTeg!qEbyv!1#k$!E6ZWw>z%zdG%s za$rx9->Z+J>%N4{65={swaGZ)(XGzV&q@)W@7+27zPW8t&lLap8J}iMaBERNUbg@7 z8B?9^IQEp(rUM=uR!sbvIKNCGV8z7aXy`MkR z^P}$lH6^~%Gq$=PeX4o!=LCu4Npto!A6Kik>ry>_;O^XKrWpbXbNg@HyL7l^4sYBG z{gX`0j0}v68xI&X?lq7F#dY#EPc0sPf3X5{=a{Ob{zMQ z`qG)eTA#3;)$8-OkB9aj*;&%IO}94t%%#t6clD1w$U1!3gjuI{+TEx_e9O6J9%Sua z&7=Q0#_D{Ic_?T5yAv>AB-zo?y~ diff --git a/revocation/crl/testdata/valid/root.cer b/revocation/crl/testdata/valid/root.cer deleted file mode 100644 index 9d2132e7f1e352fabac7eafb231488b5da91ffb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1391 zcmXqLV$C*aVh&!w%*4pVB*@StaDKxjhsTjF$q#lXH+3@@@Un4gwRyCC=VfH%W@Rw& zH{>?pWMd9xVH0Kw4K~y?PzQ0igcUsVN>YpRQcDzqQMi96N{2F6x@sQ zOA8D|4TM2TnT2^ggM-`^g7WiA6e0`_)UeSUJ_S;M+V-u>HW)=goaf7yL{%}fvF;1?F_{JHX*^)7mb z_cWAjyQP1@qPLp4KvBB%lYz~z{&jb6C9i%h=6|S9(7WzD_ly5q%k{o&s`h%|Bc#ex z(95j3;9;=J8{wPpB=-w!_Uf_kT$~tqZ%sS8l;RAn=gy-c5l%vESRjulRoaDHHpQelw1#&mWmj<25Ut_nWV1qwMTG%s)L@ zZ#3Rz-J*5P@#PxEvZ-ABH|}5EDDklY(M=kbokat@+bL(=ez`Qo=d9_8$g;*;h-`WLMh;lRc_g>Iv-DFqo zCF5PpD)i^rs|NwXHO`YuHlHea-Y3t;=GdnK4#`;nE(6$dNYTB&bR(NQ2+$oz?wqHJLsjX!HYm3h*_fBZ@a%uek ze*2NA(-ox)>ah}I#svAgPldH?sMd^L9VXJTe#U|j5E;9$T9Os}&1 zjEw(TSb({M&43@o7Y6ZJ4VZzHfhFz=l^iUlGsD^9O_ z?o;@C)1`#9mMgeli7SS+ehlD?e0}ag-X~KPhVT7{&D4o6YKug*3J5*#Pa(8&H7gpwUsuC^Ywq~GKr43@rUtb$j%*V zXSzC!JAHIpY?|)Bn-;WsJ~s2)HigcR z-KW{sqcnToqipMNtEK|qJDkTmPjj*R=DdjQJNf?H>f^h&YWulf^SYpR=4sI>j;y6q zAB!&hzU1vmo%p4{|F6+t(%W~vdiUeP>Iq_(+2h=TYs}f5dM+QCHs|Whty&MJN;P<_ z^RZ+cWl3o${YKsGG(t*4WP8`@Gk9!gJ;MzXRq} z=D1zmBD#56Ufpb-X;wRebnUN2Km5&csO6u^ip8C`)?_`D(Av1dIWhXO{2lAwvQN4% zdQ0z%8|T;t|E@mm82|syq6>)@52x)|6WeWmz4WT_ftiBq<~klMDs9=v&JYE{~5g;AP{~YV&CO&dbQi&B|cl zZ^&)H$;KSY!Y0fV8f>U(pbp}22`hN!m82HsrIsiJrzV#cWtLPb1f>?ICKe@UD7Y8p zmlha`8VG}wG7Ix~1_!w-1m)+KC`1?<$cghB85md^m>7UT6p(9bU}i>{#klr@pXRpDI%e!;XU(K zEV7jR+GOLj(l76;^&6z(8 z#AA0A<^OIxy7p3Agsu4T=bXDgYJYFLbMRJS>=n1iXV$77?AVi#UYfS~qr~?`G0%ek zTXk%6U;1BI;?)e!a{IZ#KhHBh{kp6`Tx5OnlK(R|Po8@xcsbiYk5<`*nd?+bcMG2h zV*dIzaAEgtQ6^?a2FArrj2yraVKLwX2B<7QBjbM-7GNT1Gmr)GRarnG&7sZ4$jZvj z%mimK8VG@;g+aGrR#$eo~%5Jf`OcY%mS$e637aZkrgU|Y*%2BHjp&nU}H;f0^%a4a^@zWr&>?>x!W!N-s;l2=W2SzrWwg=OMT_0*&3%7h3Gae zcy;*g4~6~lXSNqGY|pd)7B}VI6NUN9-gj?ee!gg{n9amW8&X z%-(q}=2Oav1N$lu`1j?y@Wf5pt@piK;Nc5d7tPy|3U8BlD*g~sn=(0kfov+vK`y0r z0=&-C7fQ(2J$TS&zBOv&UW5JZdD>e475$`H4}X=I{vmG7;iWsWKIrYSHs1Np``(#9 zPu90^x7i;EbvFB!@z6{>t8eDT|4SW~n`}RDA=%Q@vNZ40uCf!8nO?5+&JjPy!*p|R z$I5B||t$-I`!kiAsP`9|pUq9dAo-;c!loml7AVsQOaYrMq5%H7Z7 z3cA@JwoN{~v;R(Fp{myU`)^ePf-<@%-FbR#>*HIs7us`L6b;ukef_<2^@&b#+lM|+ zE%?6e)!sX;QRMa2+qMeJ>mn~d`VsLndWXl^e=+`In*ZcNmDisT+|c`~X7U7a{l9A# zak{(Ne|WiJ`+p7J45Mr5adMf9C-3+=w_Bh4Qjqhqe53GGU!%tR7QwBtb+KuhuXfyh uGIi_Otzkk=XOH+DQ?+mj$bEB;AyneuOV5-mey66-*%E!Ac*W`+?uP)U>;Po| From 99ad04c1df2b5833381515942200adf397b42b29 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 13:14:00 +0000 Subject: [PATCH 035/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 41 +++++++++++--- revocation/crl/crl_test.go | 102 ++++++++++++++++++++++++++++++++--- revocation/result/errors.go | 7 ++- revocation/result/results.go | 2 +- 4 files changed, 136 insertions(+), 16 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index f977cebf..7776e26e 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -18,6 +18,7 @@ package crl import ( "context" "crypto/x509" + "encoding/asn1" "errors" "fmt" "io" @@ -30,6 +31,10 @@ import ( "github.com/notaryproject/notation-core-go/revocation/result" ) +var ( + oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} +) + // Options specifies values that are needed to check CRL type Options struct { HTTPClient *http.Client @@ -121,7 +126,8 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signi for _, revocationEntry := range baseCRL.RevokedCertificateEntries { if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { - if err := validateRevocationEntry(revocationEntry); err != nil { + extensions, err := parseEntryExtension(revocationEntry) + if err != nil { return &result.CertRevocationResult{ Result: result.ResultUnknown, CRLResults: []*result.CRLResult{ @@ -134,7 +140,10 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signi } // validate revocation time - if !signingTime.IsZero() && signingTime.Before(revocationEntry.RevocationTime) { + if !signingTime.IsZero() && !extensions.invalidityDate.IsZero() && + signingTime.Before(extensions.invalidityDate) { + // signing time is before the invalidity date which means the + // certificate is not revoked at the time of signing. continue } @@ -179,15 +188,35 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signi } } -func validateRevocationEntry(entry x509.RevocationListEntry) error { +type entryExtensions struct { + // invalidityDate is the date when the key is invalid. + invalidityDate time.Time +} + +func parseEntryExtension(entry x509.RevocationListEntry) (entryExtensions, error) { + extensions := entryExtensions{} // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) for _, ext := range entry.ExtraExtensions { - if ext.Critical { - return fmt.Errorf("CRL entry contains unsupported critical extension: %v", ext.Id) + switch { + case ext.Id.Equal(oidInvalidityDate): + var invalidityDate time.Time + rest, err := asn1.UnmarshalWithParams(ext.Value, &invalidityDate, "generalized") + if err != nil { + return entryExtensions{}, fmt.Errorf("failed to parse invalidity date: %w", err) + } + if len(rest) > 0 { + return entryExtensions{}, fmt.Errorf("invalid invalidity date extension") + } + + extensions.invalidityDate = invalidityDate + default: + if ext.Critical { + return entryExtensions{}, fmt.Errorf("CRL entry contains unsupported critical extension: %v", ext.Id) + } } } - return nil + return extensions, nil } func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 958d8b0d..66f6d478 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -19,6 +19,7 @@ import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "fmt" "io" "math/big" @@ -216,13 +217,28 @@ func TestCheckRevocation(t *testing.T) { } }) - t.Run("revoked but signing time is before revocation time", func(t *testing.T) { + t.Run("revoked but signing time is before invalidityDate", func(t *testing.T) { + invalidityDate := time.Now().Add(time.Hour) + invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) + if err != nil { + t.Fatal(err) + } + + extensions := []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: invalidityDateBytes, + }, + } + baseCRL := &x509.RevocationList{ RevokedCertificateEntries: []x509.RevocationListEntry{ { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(time.Hour), + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Now().Add(time.Hour), + ExtraExtensions: extensions, }, }, } @@ -235,6 +251,40 @@ func TestCheckRevocation(t *testing.T) { } }) + t.Run("revoked; signing time is after invalidityDate", func(t *testing.T) { + invalidityDate := time.Now().Add(-time.Hour) + invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) + if err != nil { + t.Fatal(err) + } + + extensions := []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: invalidityDateBytes, + }, + } + + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + ReasonCode: int(result.CRLReasonCodeCertificateHold), + RevocationTime: time.Now().Add(-time.Hour), + ExtraExtensions: extensions, + }, + }, + } + r := checkRevocation(cert, baseCRL, signingTime) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + t.Run("revoked and signing time is zero", func(t *testing.T) { baseCRL := &x509.RevocationList{ RevokedCertificateEntries: []x509.RevocationListEntry{ @@ -381,8 +431,8 @@ func TestCheckRevocation(t *testing.T) { }) } -func TestValidateRevocationEntry(t *testing.T) { - t.Run("invalid extension", func(t *testing.T) { +func TestParseEntryExtension(t *testing.T) { + t.Run("unsupported critical extension", func(t *testing.T) { entry := x509.RevocationListEntry{ ExtraExtensions: []pkix.Extension{ { @@ -391,7 +441,7 @@ func TestValidateRevocationEntry(t *testing.T) { }, }, } - if err := validateRevocationEntry(entry); err == nil { + if _, err := parseEntryExtension(entry); err == nil { t.Fatal("expected error") } }) @@ -405,10 +455,46 @@ func TestValidateRevocationEntry(t *testing.T) { }, }, } - if err := validateRevocationEntry(entry); err != nil { + if _, err := parseEntryExtension(entry); err != nil { t.Fatal(err) } }) + + t.Run("parse invalidityDate error", func(t *testing.T) { + + // create a time and marshal it to be generalizedTime + invalidityDate := time.Now() + invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) + if err != nil { + t.Fatal(err) + } + + entry := x509.RevocationListEntry{ + ExtraExtensions: []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: invalidityDateBytes, + }, + }, + } + extensions, err := parseEntryExtension(entry) + if err != nil { + t.Fatal(err) + } + + if extensions.invalidityDate.IsZero() { + t.Fatal("expected invalidityDate") + } + }) +} + +// marshalGeneralizedTimeToBytes converts a time.Time to ASN.1 GeneralizedTime bytes. +func marshalGeneralizedTimeToBytes(t time.Time) ([]byte, error) { + // ASN.1 GeneralizedTime requires the time to be in UTC + t = t.UTC() + // Use asn1.Marshal to directly get the ASN.1 GeneralizedTime bytes + return asn1.Marshal(t) } func TestDownload(t *testing.T) { diff --git a/revocation/result/errors.go b/revocation/result/errors.go index cf7bc866..eb821eef 100644 --- a/revocation/result/errors.go +++ b/revocation/result/errors.go @@ -32,9 +32,14 @@ func (e InvalidChainError) Error() string { return msg } +// OCSPFallbackErro is returned when the OCSP check result is of unknown status +// and falls back to CRL type OCSPFallbackError struct { + // OCSPErr is the error that occurred during the OCSP check OCSPErr error - CRLErr error + + // CRLErr is the error that occurred during the CRL check + CRLErr error } func (e OCSPFallbackError) Error() string { diff --git a/revocation/result/results.go b/revocation/result/results.go index 61d5bd75..8dfa5651 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -83,7 +83,7 @@ func NewServerResult(result Result, server string, err error) *ServerResult { } } -// CRLReasonCode is CRL reason code +// CRLReasonCode is CRL reason code (See RFC 5280, section 5.3.1) type CRLReasonCode int const ( From 9eb5af53e3440b872210ac9a48051e5913e4dad5 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 13:30:06 +0000 Subject: [PATCH 036/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 27 ++++++++++++++++++++------- revocation/crl/crl_test.go | 6 +++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 7776e26e..0c4fe735 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -32,16 +32,29 @@ import ( ) var ( + // oidInvalidityDate is the object identifier for the invalidity date + // CRL entry extension. (See RFC 5280, Section 5.3.1) oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} ) // Options specifies values that are needed to check CRL type Options struct { - HTTPClient *http.Client + // HTTPClient is the HTTP client used to download CRL + HTTPClient *http.Client + + // SigningTime is the time when the certificate's private key is used to + // sign the data. SigningTime time.Time } // CertCheckStatus checks the revocation status of a certificate using CRL +// +// The function checks the revocation status of the certificate by downloading +// the CRL from the CRL distribution points specified in the certificate. +// +// If the invalidity date extension is present in the CRL entry and SigningTime +// is not zero, the certificate is considered revoked if the SigningTime is +// after the invalidity date. (See RFC 5280, Section 5.3.2) func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { if opts.HTTPClient == nil { return &result.CertRevocationResult{Error: errors.New("invalid input: a non-nil httpClient must be specified")} @@ -57,7 +70,7 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts O baseCRL, err := download(ctx, crlURL, opts.HTTPClient) if err != nil { crlResults[i] = &result.CRLResult{ - Error: err, + Error: fmt.Errorf("failed to download CRL from %s: %w", crlURL, err), } continue } @@ -126,7 +139,7 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signi for _, revocationEntry := range baseCRL.RevokedCertificateEntries { if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { - extensions, err := parseEntryExtension(revocationEntry) + extensions, err := parseEntryExtensions(revocationEntry) if err != nil { return &result.CertRevocationResult{ Result: result.ResultUnknown, @@ -193,9 +206,8 @@ type entryExtensions struct { invalidityDate time.Time } -func parseEntryExtension(entry x509.RevocationListEntry) (entryExtensions, error) { +func parseEntryExtensions(entry x509.RevocationListEntry) (entryExtensions, error) { extensions := entryExtensions{} - // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) for _, ext := range entry.ExtraExtensions { switch { case ext.Id.Equal(oidInvalidityDate): @@ -211,6 +223,7 @@ func parseEntryExtension(entry x509.RevocationListEntry) (entryExtensions, error extensions.invalidityDate = invalidityDate default: if ext.Critical { + // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) return entryExtensions{}, fmt.Errorf("CRL entry contains unsupported critical extension: %v", ext.Id) } } @@ -223,7 +236,7 @@ func download(ctx context.Context, crlURL string, client *http.Client) (*x509.Re // validate URL parsedURL, err := url.Parse(crlURL) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid CRL URL: %w", err) } if strings.ToLower(parsedURL.Scheme) != "http" { return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme) @@ -242,7 +255,7 @@ func download(ctx context.Context, crlURL string, client *http.Client) (*x509.Re // check response if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("failed to download CRL from %s: %s", crlURL, resp.Status) + return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) } // parse CRL diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 66f6d478..5cc575d4 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -441,7 +441,7 @@ func TestParseEntryExtension(t *testing.T) { }, }, } - if _, err := parseEntryExtension(entry); err == nil { + if _, err := parseEntryExtensions(entry); err == nil { t.Fatal("expected error") } }) @@ -455,7 +455,7 @@ func TestParseEntryExtension(t *testing.T) { }, }, } - if _, err := parseEntryExtension(entry); err != nil { + if _, err := parseEntryExtensions(entry); err != nil { t.Fatal(err) } }) @@ -478,7 +478,7 @@ func TestParseEntryExtension(t *testing.T) { }, }, } - extensions, err := parseEntryExtension(entry) + extensions, err := parseEntryExtensions(entry) if err != nil { t.Fatal(err) } From 38d04ce0ba20a7d908693ca5aa3a9e60519eaf03 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 13:36:20 +0000 Subject: [PATCH 037/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl_test.go | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 5cc575d4..b52d84a6 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -460,7 +460,7 @@ func TestParseEntryExtension(t *testing.T) { } }) - t.Run("parse invalidityDate error", func(t *testing.T) { + t.Run("parse invalidityDate", func(t *testing.T) { // create a time and marshal it to be generalizedTime invalidityDate := time.Now() @@ -487,6 +487,45 @@ func TestParseEntryExtension(t *testing.T) { t.Fatal("expected invalidityDate") } }) + + t.Run("parse invalidityDate with error", func(t *testing.T) { + // invalid invalidityDate extension + entry := x509.RevocationListEntry{ + ExtraExtensions: []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: []byte{0x00, 0x01, 0x02, 0x03}, + }, + }, + } + _, err := parseEntryExtensions(entry) + if err == nil { + t.Fatal("expected error") + } + + // invalidityDate extension with extra bytes + invalidityDate := time.Now() + invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) + if err != nil { + t.Fatal(err) + } + invalidityDateBytes = append(invalidityDateBytes, 0x00) + + entry = x509.RevocationListEntry{ + ExtraExtensions: []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: invalidityDateBytes, + }, + }, + } + _, err = parseEntryExtensions(entry) + if err == nil { + t.Fatal("expected error") + } + }) } // marshalGeneralizedTimeToBytes converts a time.Time to ASN.1 GeneralizedTime bytes. From 6a5357e6e9091aff36ccd59027d3704631576d5e Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 24 Jul 2024 13:41:06 +0000 Subject: [PATCH 038/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 2 +- revocation/ocsp/ocsp.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 0c4fe735..1df2edd2 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -152,7 +152,7 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signi } } - // validate revocation time + // validate signingTime and invalidityDate if !signingTime.IsZero() && !extensions.invalidityDate.IsZero() && signingTime.Before(extensions.invalidityDate) { // signing time is before the invalidity date which means the diff --git a/revocation/ocsp/ocsp.go b/revocation/ocsp/ocsp.go index 2afd0f80..e6996d53 100644 --- a/revocation/ocsp/ocsp.go +++ b/revocation/ocsp/ocsp.go @@ -115,6 +115,7 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { return certResults, nil } +// CertCheckStatus checks the revocation status of a certificate using OCSP func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { if !SupportOCSP(cert) { // OCSP not enabled for this certificate. From 3f9a259c753f32be2de23278c9b9594492a4dea3 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 25 Jul 2024 02:36:44 +0000 Subject: [PATCH 039/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 12 ++++++------ revocation/crl/crl_test.go | 17 +++++++++++++++-- revocation/revocation.go | 18 +++++++++++++----- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 1df2edd2..ed465722 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -99,16 +99,16 @@ func SupportCRL(cert *x509.Certificate) bool { } func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { - // after NextUpdate time, new CRL will be issued. (See RFC 5280, Section 5.1.2.5) - if !crl.NextUpdate.IsZero() && time.Now().After(crl.NextUpdate) { - return fmt.Errorf("CRL is expired: %v", crl.NextUpdate) - } - // check signature if err := crl.CheckSignatureFrom(issuer); err != nil { return fmt.Errorf("CRL signature verification failed: %w", err) } + // check validity + if !crl.NextUpdate.IsZero() && time.Now().After(crl.NextUpdate) { + return fmt.Errorf("CRL is expired: %v", crl.NextUpdate) + } + // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) for _, ext := range crl.Extensions { if ext.Critical { @@ -134,7 +134,7 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signi // // If the certificate is revoked with CertificateHold, it is temporarily // revoked. If the certificate is shown in the CRL with RemoveFromCRL, - // its revocation is no longer valid. + // it is unrevoked. var tempRevokedEntries []x509.RevocationListEntry for _, revocationEntry := range baseCRL.RevokedCertificateEntries { diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index b52d84a6..38d20b26 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -110,11 +110,24 @@ func TestCertCheckStatus(t *testing.T) { func TestValidate(t *testing.T) { t.Run("expired CRL", func(t *testing.T) { - crl := &x509.RevocationList{ + chain := testhelper.GetRevokableRSAChain(1, false, true) + issuerCert := chain[0].Cert + issuerKey := chain[0].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(-time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) } - if err := validate(crl, &x509.Certificate{}); err == nil { + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatal(err) + } + + if err := validate(crl, issuerCert); err == nil { t.Fatal("expected error") } }) diff --git a/revocation/revocation.go b/revocation/revocation.go index f00bf624..43b878dd 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -119,11 +119,20 @@ func NewWithOptions(opts *Options) (Revocation, error) { }, nil } -// Validate checks the revocation status for a certificate chain using OCSP or -// CRL and returns an array of CertRevocationResults that contain the results -// and any errors that are encountered during the process. +// Validate checks the revocation status for a certificate chain using OCSP and +// CRL if OCSP is not available. It returns an array of CertRevocationResults +// that contain the results and any errors that are encountered during the +// process. // // The certificate chain is expected to be in the order of leaf to root. +// +// This function tries OCSP and falls back to CRL when: +// - OCSP is not supported by the certificate +// - OCSP returns an unknown status +// +// When OCSP returns an unknown status, the function will try to check the +// certificate status using CRL and return certificate result with an +// result.OCSPFallbackError. func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { if len(certChain) == 0 { return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} @@ -169,9 +178,8 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti go func(i int, cert *x509.Certificate) { defer wg.Done() ocspResult := ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) - - // try CRL check if OCSP result is unknown if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.SupportCRL(cert) { + // try CRL check if OCSP result is unknown crlResult := crl.CertCheckStatus(r.ctx, cert, certChain[i+1], crlOpts) crlResult.Error = result.OCSPFallbackError{ OCSPErr: ocspResult.Error, From 6bf3ca28d00a09bda6e67776e794df1fcdd7dff8 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 25 Jul 2024 02:51:05 +0000 Subject: [PATCH 040/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index ed465722..e305c0b7 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -33,7 +33,7 @@ import ( var ( // oidInvalidityDate is the object identifier for the invalidity date - // CRL entry extension. (See RFC 5280, Section 5.3.1) + // CRL entry extension. (See RFC 5280, Section 5.3.2) oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} ) From 751397b1d949ee208f485cce9dd6c4195632786b Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 25 Jul 2024 03:11:26 +0000 Subject: [PATCH 041/110] fix: add CRL size limit Signed-off-by: Junjie Gao --- revocation/crl/crl.go | 26 ++++++++++++++++---------- revocation/crl/crl_test.go | 9 +++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index e305c0b7..74cafd81 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -31,11 +31,12 @@ import ( "github.com/notaryproject/notation-core-go/revocation/result" ) -var ( - // oidInvalidityDate is the object identifier for the invalidity date - // CRL entry extension. (See RFC 5280, Section 5.3.2) - oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} -) +// oidInvalidityDate is the object identifier for the invalidity date +// CRL entry extension. (See RFC 5280, Section 5.3.2) +var oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} + +// maxCRLSize is the maximum size of CRL in bytes +const maxCRLSize = 10 << 20 // 10 MiB // Options specifies values that are needed to check CRL type Options struct { @@ -245,11 +246,11 @@ func download(ctx context.Context, crlURL string, client *http.Client) (*x509.Re // download CRL req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create CRL request: %w", err) } resp, err := client.Do(req) if err != nil { - return nil, err + return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() @@ -258,10 +259,15 @@ func download(ctx context.Context, crlURL string, client *http.Client) (*x509.Re return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) } - // parse CRL - data, err := io.ReadAll(resp.Body) + // read with size limit + limitedReader := io.LimitReader(resp.Body, maxCRLSize) + data, err := io.ReadAll(limitedReader) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read CRL response: %w", err) } + if len(data) == maxCRLSize { + return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize) + } + return x509.ParseRevocationList(data) } diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go index 38d20b26..854c3d99 100644 --- a/revocation/crl/crl_test.go +++ b/revocation/crl/crl_test.go @@ -598,6 +598,15 @@ func TestDownload(t *testing.T) { t.Fatal("expected error") } }) + + t.Run("exceed the size limit", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, + }) + if err == nil { + t.Fatal("expected error") + } + }) } type errorRoundTripperMock struct{} From 3170db30def80ac6f452d279a7c843a9eebb5953 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 25 Jul 2024 08:39:39 +0000 Subject: [PATCH 042/110] fix: restore workflow and update NewWithOptions Signed-off-by: Junjie Gao --- .github/workflows/build.yml | 8 ++------ .github/workflows/codeql.yml | 8 ++------ .github/workflows/license-checker.yml | 8 ++------ revocation/revocation.go | 15 +++++---------- revocation/revocation_test.go | 21 +++++++-------------- 5 files changed, 18 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ebdbf627..184a10f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,13 +15,9 @@ name: Build on: push: - branches: - - main - - crl + branches: main pull_request: - branches: - - main - - crl + branches: main jobs: build: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index db702a3b..9a4272b1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,13 +15,9 @@ name: "CodeQL" on: push: - branches: - - main - - crl + branches: main pull_request: - branches: - - main - - crl + branches: main schedule: - cron: '38 15 * * 1' diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index cf044ee1..539c4a5b 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -15,13 +15,9 @@ name: License Checker on: push: - branches: - - main - - crl + branches: main pull_request: - branches: - - main - - crl + branches: main permissions: contents: write diff --git a/revocation/revocation.go b/revocation/revocation.go index 43b878dd..4dce8526 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -41,9 +41,6 @@ type Revocation interface { // Options specifies values that are needed to check revocation type Options struct { - // Ctx is a required context used for the revocation check - Ctx context.Context - // OCSPHTTPClient is a required HTTP client for OCSP request OCSPHTTPClient *http.Client @@ -68,8 +65,7 @@ func New(httpClient *http.Client) (Revocation, error) { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } - return NewWithOptions(&Options{ - Ctx: context.Background(), + return NewWithOptions(context.Background(), &Options{ OCSPHTTPClient: httpClient, CRLHTTPClient: httpClient, CertChainPurpose: ocsp.PurposeCodeSigning, @@ -83,8 +79,7 @@ func NewTimestamp(httpClient *http.Client) (Revocation, error) { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } - return NewWithOptions(&Options{ - Ctx: context.Background(), + return NewWithOptions(context.Background(), &Options{ OCSPHTTPClient: httpClient, CRLHTTPClient: httpClient, CertChainPurpose: ocsp.PurposeTimestamping, @@ -92,8 +87,8 @@ func NewTimestamp(httpClient *http.Client) (Revocation, error) { } // NewWithOptions constructs a revocation object with the specified options -func NewWithOptions(opts *Options) (Revocation, error) { - if opts.Ctx == nil { +func NewWithOptions(ctx context.Context, opts *Options) (Revocation, error) { + if ctx == nil { return nil, errors.New("invalid input: a non-nil context must be specified") } @@ -112,7 +107,7 @@ func NewWithOptions(opts *Options) (Revocation, error) { } return &revocation{ - ctx: opts.Ctx, + ctx: ctx, ocspHTTPClient: opts.OCSPHTTPClient, crlHTTPClient: opts.CRLHTTPClient, certChainPurpose: opts.CertChainPurpose, diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index 7799e358..5ae87b02 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -982,8 +982,7 @@ func TestCRL(t *testing.T) { t.Run("CRL check valid", func(t *testing.T) { chain := testhelper.GetRevokableRSAChain(3, false, true) - revocationClient, err := NewWithOptions(&Options{ - Ctx: context.Background(), + revocationClient, err := NewWithOptions(context.Background(), &Options{ CRLHTTPClient: &http.Client{ Timeout: 5 * time.Second, Transport: &crlRoundTripper{ @@ -1018,8 +1017,7 @@ func TestCRL(t *testing.T) { t.Run("CRL check with revoked status", func(t *testing.T) { chain := testhelper.GetRevokableRSAChain(3, false, true) - revocationClient, err := NewWithOptions(&Options{ - Ctx: context.Background(), + revocationClient, err := NewWithOptions(context.Background(), &Options{ CRLHTTPClient: &http.Client{ Timeout: 5 * time.Second, Transport: &crlRoundTripper{ @@ -1068,8 +1066,7 @@ func TestCRL(t *testing.T) { t.Run("OCSP fallback to CRL", func(t *testing.T) { chain := testhelper.GetRevokableRSAChain(3, true, true) - revocationClient, err := NewWithOptions(&Options{ - Ctx: context.Background(), + revocationClient, err := NewWithOptions(context.Background(), &Options{ CRLHTTPClient: &http.Client{ Timeout: 5 * time.Second, Transport: &crlRoundTripper{ @@ -1121,24 +1118,21 @@ func TestCRL(t *testing.T) { func TestNewWithOptions(t *testing.T) { t.Run("nil ctx", func(t *testing.T) { - _, err := NewWithOptions(&Options{}) + _, err := NewWithOptions(context.Background(), &Options{}) if err == nil { t.Error("Expected NewWithOptions to fail with an error, but it succeeded") } }) t.Run("nil OCSP HTTP Client", func(t *testing.T) { - _, err := NewWithOptions(&Options{ - Ctx: context.Background(), - }) + _, err := NewWithOptions(context.Background(), &Options{}) if err == nil { t.Error("Expected NewWithOptions to fail with an error, but it succeeded") } }) t.Run("nil CRL HTTP Client", func(t *testing.T) { - _, err := NewWithOptions(&Options{ - Ctx: context.Background(), + _, err := NewWithOptions(context.Background(), &Options{ OCSPHTTPClient: &http.Client{}, }) if err == nil { @@ -1147,8 +1141,7 @@ func TestNewWithOptions(t *testing.T) { }) t.Run("invalid CertChainPurpose", func(t *testing.T) { - _, err := NewWithOptions(&Options{ - Ctx: context.Background(), + _, err := NewWithOptions(context.Background(), &Options{ OCSPHTTPClient: &http.Client{}, CRLHTTPClient: &http.Client{}, CertChainPurpose: -1, From 041a63d49c21a6d198c372bb0a08418debc9140b Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 25 Jul 2024 08:43:07 +0000 Subject: [PATCH 043/110] fix: update Signed-off-by: Junjie Gao --- revocation/revocation_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index 5ae87b02..b938bd83 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -1118,7 +1118,7 @@ func TestCRL(t *testing.T) { func TestNewWithOptions(t *testing.T) { t.Run("nil ctx", func(t *testing.T) { - _, err := NewWithOptions(context.Background(), &Options{}) + _, err := NewWithOptions(nil, &Options{}) if err == nil { t.Error("Expected NewWithOptions to fail with an error, but it succeeded") } From e170b905630df3150daf4b0f5225fb9ab6be5314 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 25 Jul 2024 08:43:52 +0000 Subject: [PATCH 044/110] fix: update Signed-off-by: Junjie Gao --- revocation/revocation_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index b938bd83..0ca1ec8a 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -1118,7 +1118,8 @@ func TestCRL(t *testing.T) { func TestNewWithOptions(t *testing.T) { t.Run("nil ctx", func(t *testing.T) { - _, err := NewWithOptions(nil, &Options{}) + var ctx context.Context = nil + _, err := NewWithOptions(ctx, &Options{}) if err == nil { t.Error("Expected NewWithOptions to fail with an error, but it succeeded") } From 1c68a1797154b405694d3b27f8042536ca57f85b Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 26 Jul 2024 05:44:43 +0000 Subject: [PATCH 045/110] fix: update Signed-off-by: Junjie Gao --- revocation/revocation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revocation/revocation.go b/revocation/revocation.go index 4dce8526..ad18a716 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -204,7 +204,7 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti } } - // Last is root cert, which will never be revoked by OCSP + // Last is root cert, which will never be revoked by OCSP or CRL certResults[len(certChain)-1] = &result.CertRevocationResult{ Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{{ From a56eb87d8946cd7bd42b4121b8c556cc191f8835 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 29 Nov 2023 16:52:49 +0800 Subject: [PATCH 046/110] 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 +} From 5e5ddf3c3d8f15b0c6e7ebd06e472718534ee6e0 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 7 Dec 2023 15:41:07 +0800 Subject: [PATCH 047/110] test: add unit test Signed-off-by: Junjie Gao --- internal/encoding/asn1/asn1.go | 74 ++++-- internal/encoding/asn1/asn1_test.go | 333 ++++++++++++++++++++++---- internal/encoding/asn1/common.go | 6 + internal/encoding/asn1/common_test.go | 86 +++++++ 4 files changed, 432 insertions(+), 67 deletions(-) create mode 100644 internal/encoding/asn1/common_test.go diff --git a/internal/encoding/asn1/asn1.go b/internal/encoding/asn1/asn1.go index 28d79049..8e58294e 100644 --- a/internal/encoding/asn1/asn1.go +++ b/internal/encoding/asn1/asn1.go @@ -12,8 +12,14 @@ // 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 +// Note: +// - DER is a subset of BER. +// - Indefinite length is not supported. +// - The length of the encoded data must fit the memory space of the int type (4 bytes). +// +// Reference: +// - http://luca.ntop.org/Teaching/Appunti/asn1.html +// - ISO/IEC 8825-1 package asn1 import ( @@ -71,11 +77,18 @@ func ConvertToDER(ber []byte) ([]byte, error) { } // 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 +// To get the DER of `r`, encode the values // in the returned slice in order. +// +// Parameters: +// r - The input byte slice. +// +// Return: +// []value - The returned value, which is the flat slice of ASN.1 values, +// contains the nodes from a depth-first traversal. +// error - An error that can occur during the decoding process. +// +// Reference: ISO/IEC 8825-1: 8.1.1.3 func decode(r []byte) ([]value, error) { // prepare the first value identifier, content, r, err := decodeMetadata(r) @@ -150,11 +163,21 @@ func decode(r []byte) ([]value, error) { // 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. +// Parameters: +// r - The input byte slice. +// +// Return: +// []byte - The identifier octets. +// []byte - The content octets. +// []byte - The subsequent octets after the value. +// error - An error that can occur during the decoding process. +// +// Reference: ISO/IEC 8825-1: 8.1.1.3 func decodeMetadata(r []byte) ([]byte, []byte, []byte, error) { + // structure of an encoding (primitive or constructed) + // +----------------+----------------+----------------+ + // | identifier | length | content | + // +----------------+----------------+----------------+ identifier, r, err := decodeIdentifier(r) if err != nil { return nil, nil, nil, err @@ -172,9 +195,15 @@ func decodeMetadata(r []byte) ([]byte, []byte, []byte, error) { // 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. +// Parameters: +// r - The input byte slice from which the identifier octets are to be decoded. +// +// Returns: +// []byte - The identifier octets decoded from the input byte slice. +// []byte - The remaining part of the input byte slice after the identifier octets. +// error - An error that can occur during the decoding process. +// +// Reference: ISO/IEC 8825-1: 8.1.2 func decodeIdentifier(r []byte) ([]byte, []byte, error) { if len(r) < 1 { return nil, nil, ErrEarlyEOF @@ -184,6 +213,7 @@ func decodeIdentifier(r []byte) ([]byte, []byte, error) { offset++ // high-tag-number form + // Reference: ISO/IEC 8825-1: 8.1.2.4 if b&0x1f == 0x1f { for offset < len(r) && r[offset]&0x80 == 0x80 { offset++ @@ -199,9 +229,15 @@ func decodeIdentifier(r []byte) ([]byte, []byte, error) { // 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. +// Parameters: +// r - The input byte slice from which the length octets are to be decoded. +// +// Returns: +// int - The length decoded from the input byte slice. +// []byte - The remaining part of the input byte slice after the length octets. +// error - An error that can occur during the decoding process. +// +// Reference: ISO/IEC 8825-1: 8.1.3 func decodeLength(r []byte) (int, []byte, error) { if len(r) < 1 { return 0, nil, ErrEarlyEOF @@ -212,16 +248,19 @@ func decodeLength(r []byte) (int, []byte, error) { if b < 0x80 { // short form + // Reference: ISO/IEC 8825-1: 8.1.3.4 return int(b), r[offset:], nil } else if b == 0x80 { // Indefinite-length method is not supported. + // Reference: ISO/IEC 8825-1: 8.1.3.6.1 return 0, nil, ErrUnsupportedIndefiniteLength } // long form + // Reference: ISO/IEC 8825-1: 8.1.3.5 n := int(b & 0x7f) if n > 4 { - // length must fit the memory space of the int type. + // length must fit the memory space of the int type (4 bytes). return 0, nil, ErrUnsupportedLength } if offset+n >= len(r) { @@ -242,6 +281,7 @@ func decodeLength(r []byte) (int, []byte, error) { // isPrimitive returns true if the first identifier octet is marked // as primitive. +// Reference: ISO/IEC 8825-1: 8.1.2.5 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 index 90ca3aea..3cc94795 100644 --- a/internal/encoding/asn1/asn1_test.go +++ b/internal/encoding/asn1/asn1_test.go @@ -14,68 +14,301 @@ package asn1 import ( - "encoding/asn1" + "fmt" "reflect" "testing" ) func TestConvertToDER(t *testing.T) { - type data struct { - Type asn1.ObjectIdentifier - Value []byte - } + testData := []struct { + name string + ber []byte + der []byte + expectError bool + }{ + { + name: "Constructed value", + 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: []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2d, - 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, + // Type identifier + 0x06, + // Type length + 0x09, + // Type content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, + + // Value identifier + 0x04, + // Value length in BER + 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, + }, + expectError: false, }, - } + { + name: "Primitive value", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0x20, + // length + 0x81, 0x01, + // content + 0x01, + }, + der: []byte{ + // Primitive value + // identifier + 0x1f, 0x20, + // length + 0x01, + // content + 0x01, + }, + expectError: false, + }, + { + name: "Constructed value in constructed value", + ber: []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2d, - ber := []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2e, + // Constructed value identifier + 0x26, + // Type length + 0x2b, - // 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, 0x28, + // Value content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 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: []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2c, - // 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, - } + // Constructed value identifier + 0x26, + // Type length + 0x2a, - der, err := ConvertToDER(ber) - if err != nil { - t.Errorf("ConvertToDER() error = %v", err) - return - } + // Value identifier + 0x04, + // Value length in BER + 0x28, + // Value content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 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, + }, + expectError: false, + }, + { + name: "empty", + ber: []byte{}, + der: []byte{}, + expectError: true, + }, + { + name: "identifier high tag number form", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x81, 0x01, + // content + 0x01, + }, + der: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x01, + // content + 0x01, + }, + expectError: false, + }, + { + name: "EOF for identifier high tag number form", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, + }, + der: []byte{}, + expectError: true, + }, + { + name: "EOF for length", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + }, + der: []byte{}, + expectError: true, + }, + { + name: "Unsupport indefinite-length", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x80, + }, + der: []byte{}, + expectError: true, + }, + { + name: "length greater than 4 bytes", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x85, + }, + der: []byte{}, + expectError: true, + }, + { + name: "long form length EOF ", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x84, + }, + der: []byte{}, + expectError: true, + }, + { + name: "length greater > int32", + ber: append([]byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x84, 0xFF, 0xFF, 0xFF, 0xFF, + }, make([]byte, 0xFFFFFFFF)...), + der: []byte{}, + expectError: true, + }, + { + name: "length greater than content", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x02, + }, + der: []byte{}, + expectError: true, + }, + { + name: "trailing data", + ber: []byte{ + // Primitive value + // identifier + 0x1f, 0xa0, 0x20, + // length + 0x02, + // content + 0x01, 0x02, 0x03, + }, + der: []byte{}, + expectError: true, + }, + { + name: "EOF in constructed value", + ber: []byte{ + // Constructed value + 0x30, + // Constructed value length + 0x2c, - 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 + // Constructed value identifier + 0x26, + // Type length + 0x2b, + + // Value identifier + 0x04, + // Value length in BER + 0x81, 0x28, + // Value content + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 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, + }, + expectError: true, + }, } - if !reflect.DeepEqual(got, want) { - t.Errorf("got = %v, want %v", got, want) + + for _, tt := range testData { + der, err := ConvertToDER(tt.ber) + fmt.Printf("DER: %x\n", der) + if !tt.expectError && err != nil { + t.Errorf("ConvertToDER() error = %v, but expect no error", err) + return + } + if tt.expectError && err == nil { + t.Errorf("ConvertToDER() error = nil, but expect error") + } + + if !tt.expectError && !reflect.DeepEqual(der, tt.der) { + t.Errorf("got = %v, want %v", der, tt.der) + } } } diff --git a/internal/encoding/asn1/common.go b/internal/encoding/asn1/common.go index eb93ba78..7c7110e7 100644 --- a/internal/encoding/asn1/common.go +++ b/internal/encoding/asn1/common.go @@ -18,7 +18,12 @@ import ( ) // encodeLength encodes length octets in DER. +// Reference: ISO/IEC 8825-1: 10.1 func encodeLength(w io.ByteWriter, length int) error { + if length < 0 { + return ErrUnsupportedLength + } + // DER restriction: short form must be used for length less than 128 if length < 0x80 { return w.WriteByte(byte(length)) @@ -39,6 +44,7 @@ func encodeLength(w io.ByteWriter, length int) error { } // encodedLengthSize gives the number of octets used for encoding the length. +// Reference: ISO/IEC 8825-1: 10.1 func encodedLengthSize(length int) int { if length < 0x80 { return 1 diff --git a/internal/encoding/asn1/common_test.go b/internal/encoding/asn1/common_test.go new file mode 100644 index 00000000..1dd781ca --- /dev/null +++ b/internal/encoding/asn1/common_test.go @@ -0,0 +1,86 @@ +package asn1 + +import ( + "bytes" + "testing" +) + +func TestEncodeLength(t *testing.T) { + tests := []struct { + name string + length int + want []byte + wantErr bool + }{ + { + name: "Length less than 128", + length: 127, + want: []byte{127}, + wantErr: false, + }, + { + name: "Length equal to 128", + length: 128, + want: []byte{0x81, 128}, + wantErr: false, + }, + { + name: "Length greater than 128", + length: 300, + want: []byte{0x82, 0x01, 0x2C}, + wantErr: false, + }, + { + name: "Negative length", + length: -1, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := &bytes.Buffer{} + err := encodeLength(buf, tt.length) + if (err != nil) != tt.wantErr { + t.Errorf("encodeLength() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got := buf.Bytes(); !bytes.Equal(got, tt.want) { + t.Errorf("encodeLength() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEncodedLengthSize(t *testing.T) { + tests := []struct { + name string + length int + want int + }{ + { + name: "Length less than 128", + length: 127, + want: 1, + }, + { + name: "Length equal to 128", + length: 128, + want: 2, + }, + { + name: "Length greater than 128", + length: 300, + want: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := encodedLengthSize(tt.length); got != tt.want { + t.Errorf("encodedLengthSize() = %v, want %v", got, tt.want) + } + }) + } +} From d1976f3509d03ec4655b4373d5f3bfbfa474fd3c Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 29 Jul 2024 02:25:27 +0000 Subject: [PATCH 048/110] feat: crl cache Signed-off-by: Junjie Gao --- revocation/crl/cache/blob.go | 136 ++++++++++++++++++++++++++++++++++ revocation/crl/cache/cache.go | 22 ++++++ revocation/crl/cache/dummy.go | 31 ++++++++ revocation/crl/cache/fs.go | 89 ++++++++++++++++++++++ revocation/crl/fetcher.go | 92 +++++++++++++++++++++++ 5 files changed, 370 insertions(+) create mode 100644 revocation/crl/cache/blob.go create mode 100644 revocation/crl/cache/cache.go create mode 100644 revocation/crl/cache/dummy.go create mode 100644 revocation/crl/cache/fs.go create mode 100644 revocation/crl/fetcher.go diff --git a/revocation/crl/cache/blob.go b/revocation/crl/cache/blob.go new file mode 100644 index 00000000..a46fd897 --- /dev/null +++ b/revocation/crl/cache/blob.go @@ -0,0 +1,136 @@ +package cache + +import ( + "archive/tar" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "time" +) + +const ( + // BaseCRL is the file name of the base CRL + BaseCRLFile = "base.crl" + + // Metadata is the file name of the metadata + MetadataFile = "metadata.json" +) + +// CRL is in memory representation of the CRL tarball, including CRL file and +// metadata file, which may be cached in the file system or other storage +type CRL struct { + BaseCRL *x509.RevocationList + Metadata Metadata +} + +// Metadata stores the metadata infomation of the CRL +type Metadata struct { + BaseCRL FileInfo `json:"base.crl"` +} + +// FileInfo stores the URL and creation time of the file +type FileInfo struct { + URL string `json:"url"` + CreateAt time.Time `json:"createAt"` +} + +// NewCRL creates a new CRL store with tarball format +func NewCRL(baseCRL *x509.RevocationList, url string) *CRL { + return &CRL{ + BaseCRL: baseCRL, + Metadata: Metadata{ + BaseCRL: FileInfo{ + URL: url, + CreateAt: time.Now(), + }, + }} +} + +// ParseCRLFromTarball parses the CRL blob from a tarball +func ParseCRLFromTarball(data io.Reader) (*CRL, error) { + crlBlob := &CRL{} + + // parse the tarball + tar := tar.NewReader(data) + + for { + header, err := tar.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + switch header.Name { + case BaseCRLFile: + // parse base.crl + data, err := io.ReadAll(tar) + if err != nil { + return nil, err + } + + var baseCRL *x509.RevocationList + baseCRL, err = x509.ParseRevocationList(data) + if err != nil { + return nil, err + } + + crlBlob.BaseCRL = baseCRL + case MetadataFile: + // parse metadata + var metadata Metadata + if err := json.NewDecoder(tar).Decode(&metadata); err != nil { + return nil, err + } + + crlBlob.Metadata = metadata + + default: + return nil, fmt.Errorf("unknown file in tarball: %s", header.Name) + } + } + + if crlBlob.BaseCRL == nil { + return nil, errors.New("base.crl is missing") + } + + if crlBlob.Metadata.BaseCRL.URL == "" { + return nil, errors.New("base CRL's URL is missing from metadata.json") + } + + return crlBlob, nil +} + +// SaveAsTar saves the CRL blob as a tarball, including the base CRL and +// metadata +func (c *CRL) SaveAsTarball(w io.Writer) (err error) { + tarWriter := tar.NewWriter(w) + // Add base.crl + if err := addToTar(BaseCRLFile, c.BaseCRL.Raw, tarWriter); err != nil { + return err + } + + // Add metadata.json + metadataBytes, err := json.Marshal(c.Metadata) + if err != nil { + return err + } + return addToTar(MetadataFile, metadataBytes, tarWriter) +} + +func addToTar(fileName string, data []byte, tw *tar.Writer) error { + header := &tar.Header{ + Name: fileName, + Size: int64(len(data)), + Mode: 0644, + ModTime: time.Now(), + } + if err := tw.WriteHeader(header); err != nil { + return err + } + _, err := tw.Write(data) + return err +} diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go new file mode 100644 index 00000000..3b790e1b --- /dev/null +++ b/revocation/crl/cache/cache.go @@ -0,0 +1,22 @@ +package cache + +import ( + "context" +) + +// Cache is an interface that specifies methods used for caching +type Cache interface { + // Get retrieves the content with the given key + // + // if the key does not exist, return os.ErrNotExist + Get(ctx context.Context, key string) (any, error) + + // Set stores the content with the given key + Set(ctx context.Context, key string, value any) error + + // Delete removes the content with the given key + Delete(ctx context.Context, key string) error + + // Clear removes all content + Clear(ctx context.Context) error +} diff --git a/revocation/crl/cache/dummy.go b/revocation/crl/cache/dummy.go new file mode 100644 index 00000000..10702d33 --- /dev/null +++ b/revocation/crl/cache/dummy.go @@ -0,0 +1,31 @@ +package cache + +import ( + "context" + "os" +) + +// dummyCache is a dummy cache implementation that does nothing +type dummyCache struct { +} + +// NewDummyCache creates a new dummy cache +func NewDummyCache() Cache { + return &dummyCache{} +} + +func (c *dummyCache) Get(ctx context.Context, key string) (any, error) { + return nil, os.ErrNotExist +} + +func (c *dummyCache) Set(ctx context.Context, key string, value any) error { + return nil +} + +func (c *dummyCache) Delete(ctx context.Context, key string) error { + return nil +} + +func (c *dummyCache) Clear(ctx context.Context) error { + return nil +} diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go new file mode 100644 index 00000000..1d3098a7 --- /dev/null +++ b/revocation/crl/cache/fs.go @@ -0,0 +1,89 @@ +package cache + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" +) + +const ( + // DefaultTTL is the default time to live for the cache + DefaultTTL = 24 * 7 * time.Hour + + // tempFileName is the prefix of the temporary file + tempFileName = "notation-*" +) + +// fileSystemCache builds on top of OS file system to leverage the file system +// concurrency control and atomicity +type fileSystemCache struct { + dir string + ttl time.Duration +} + +// NewFileSystemCache creates a new file system store +func NewFileSystemCache(dir string, ttl time.Duration) (Cache, error) { + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + if ttl == 0 { + ttl = DefaultTTL + } + + return &fileSystemCache{ + dir: dir, + ttl: ttl, + }, nil +} + +func (c *fileSystemCache) Get(ctx context.Context, key string) (any, error) { + f, err := os.Open(filepath.Join(c.dir, key)) + if err != nil { + return nil, err + } + defer f.Close() + + blob, err := ParseCRLFromTarball(f) + if err != nil { + return nil, err + } + + if time.Since(blob.Metadata.BaseCRL.CreateAt) > c.ttl { + return nil, os.ErrNotExist + } + + return blob, nil +} + +func (c *fileSystemCache) Set(ctx context.Context, key string, value any) error { + var crlBlob *CRL + switch v := value.(type) { + case *CRL: + crlBlob = v + default: + return fmt.Errorf("invalid value type: %T", value) + } + + tempFile, err := os.CreateTemp("", tempFileName) + if err != nil { + return err + } + if err := crlBlob.SaveAsTarball(tempFile); err != nil { + return err + } + + tempFile.Close() + + return os.Rename(tempFile.Name(), filepath.Join(c.dir, key)) +} + +func (c *fileSystemCache) Delete(ctx context.Context, key string) error { + return os.Remove(filepath.Join(c.dir, key)) +} + +func (c *fileSystemCache) Clear(ctx context.Context) error { + return os.RemoveAll(c.dir) +} diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go new file mode 100644 index 00000000..beb738ad --- /dev/null +++ b/revocation/crl/fetcher.go @@ -0,0 +1,92 @@ +package crl + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" + + "github.com/notaryproject/notation-core-go/revocation/crl/cache" +) + +// Fetcher is an interface to fetch CRL +type Fetcher interface { + // Fetch retrieves the CRL with the given URL + Fetch(ctx context.Context, url string) (crl *cache.CRL, err error) +} + +// cachedFetcher is a CRL fetcher with cache +type cachedFetcher struct { + httpClient *http.Client + cache cache.Cache +} + +// NewCachedFetcher creates a new CRL fetcher with cache +func NewCachedFetcher(httpClient *http.Client, cache cache.Cache) Fetcher { + return &cachedFetcher{ + httpClient: httpClient, + cache: cache, + } +} + +// Fetch retrieves the CRL with the given URL +func (c *cachedFetcher) Fetch(ctx context.Context, url string) (crl *cache.CRL, err error) { + fmt.Println("fetching CRL from", url) + // try to get from cache + obj, err := c.cache.Get(ctx, tarStoreName(url)) + if err != nil { + if os.IsNotExist(err) { + // fallback to fetch from remote + crlStore, err := c.download(url) + if err != nil { + return nil, err + } + return crlStore, nil + } + + return nil, err + } + + crl, ok := obj.(*cache.CRL) + if !ok { + return nil, fmt.Errorf("invalid cache object type: %T", obj) + } + + return crl, nil +} + +func (c *cachedFetcher) download(url string) (*cache.CRL, error) { + // fetch from remote + resp, err := c.httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + crl, err := x509.ParseRevocationList(data) + if err != nil { + return nil, err + } + + crlStore := cache.NewCRL(crl, url) + return crlStore, nil +} + +func tarStoreName(url string) string { + return hashURL(url) + ".tar" +} + +// hashURL hashes the URL with SHA256 and returns the hex-encoded result +func hashURL(url string) string { + hash := sha256.Sum256([]byte(url)) + return hex.EncodeToString(hash[:]) +} From 8f006202adde614fd9e81ff05258ccb8de6e4b9d Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 29 Jul 2024 07:52:46 +0000 Subject: [PATCH 049/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/blob.go | 51 +++++++++++++++++++++++++++++++----- revocation/crl/crl.go | 27 +++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/revocation/crl/cache/blob.go b/revocation/crl/cache/blob.go index a46fd897..85b4b210 100644 --- a/revocation/crl/cache/blob.go +++ b/revocation/crl/cache/blob.go @@ -2,7 +2,10 @@ package cache import ( "archive/tar" + "bytes" + "crypto/sha256" "crypto/x509" + "encoding/gob" "encoding/json" "errors" "fmt" @@ -23,6 +26,8 @@ const ( type CRL struct { BaseCRL *x509.RevocationList Metadata Metadata + + checksum [32]byte } // Metadata stores the metadata infomation of the CRL @@ -37,20 +42,52 @@ type FileInfo struct { } // NewCRL creates a new CRL store with tarball format -func NewCRL(baseCRL *x509.RevocationList, url string) *CRL { - return &CRL{ +func NewCRL(baseCRL *x509.RevocationList, url string) (*CRL, error) { + crl := &CRL{ BaseCRL: baseCRL, Metadata: Metadata{ BaseCRL: FileInfo{ URL: url, CreateAt: time.Now(), }, - }} + }, + } + + return crl, nil +} + +func (c *CRL) IsCached() bool { + // check c.checksum is empty + checksum, err := c.computCheckSum() + if err != nil { + return false + } + + return c.checksum == checksum +} + +func (c *CRL) computCheckSum() ([32]byte, error) { + toBeHashed := struct { + BaseCRL []byte + Metadata Metadata + }{ + BaseCRL: c.BaseCRL.Raw, + Metadata: c.Metadata, + } + + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + if err := enc.Encode(toBeHashed); err != nil { + return [32]byte{}, err + } + + // sha256 hash + return sha256.Sum256(buf.Bytes()), nil } // ParseCRLFromTarball parses the CRL blob from a tarball func ParseCRLFromTarball(data io.Reader) (*CRL, error) { - crlBlob := &CRL{} + crlBlob := &CRL{cached: true} // parse the tarball tar := tar.NewReader(data) @@ -106,15 +143,15 @@ func ParseCRLFromTarball(data io.Reader) (*CRL, error) { // SaveAsTar saves the CRL blob as a tarball, including the base CRL and // metadata -func (c *CRL) SaveAsTarball(w io.Writer) (err error) { +func SaveAsTarball(w io.Writer, crl *CRL) (err error) { tarWriter := tar.NewWriter(w) // Add base.crl - if err := addToTar(BaseCRLFile, c.BaseCRL.Raw, tarWriter); err != nil { + if err := addToTar(BaseCRLFile, crl.BaseCRL.Raw, tarWriter); err != nil { return err } // Add metadata.json - metadataBytes, err := json.Marshal(c.Metadata) + metadataBytes, err := json.Marshal(crl.Metadata) if err != nil { return err } diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go index 74cafd81..1ea8d04d 100644 --- a/revocation/crl/crl.go +++ b/revocation/crl/crl.go @@ -24,10 +24,12 @@ import ( "io" "net/http" "net/url" + "os" "sort" "strings" "time" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/result" ) @@ -233,6 +235,31 @@ func parseEntryExtensions(entry x509.RevocationListEntry) (entryExtensions, erro return extensions, nil } +func fetchCRL(ctx context.Context, cacheClient cache.Cache, crlURL string, client *http.Client) (*cache.CRL, error) { + // check cache + // try to get from cache + obj, err := cacheClient.Get(ctx, tarStoreName(crlURL)) + if err != nil { + if os.IsNotExist(err) { + crl, err := download(ctx, crlURL, client) + if err != nil { + return nil, err + } + + return cache.NewCRL(crl, crlURL), nil + } + + return nil, err + } + + crl, ok := obj.(*cache.CRL) + if !ok { + return nil, fmt.Errorf("invalid cache object type: %T", obj) + } + + return crl, nil +} + func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { // validate URL parsedURL, err := url.Parse(crlURL) From 6e82a2f239452e2d4e79cd5129e75595ee750eb0 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 1 Aug 2024 08:36:17 +0000 Subject: [PATCH 050/110] fix: update cache Signed-off-by: Junjie Gao --- revocation/crl/cache/cache.go | 21 ++++- revocation/crl/cache/{blob.go => crl.go} | 104 ++++++++++++----------- revocation/crl/cache/error.go | 14 +++ revocation/crl/cache/fs.go | 2 +- 4 files changed, 91 insertions(+), 50 deletions(-) rename revocation/crl/cache/{blob.go => crl.go} (60%) create mode 100644 revocation/crl/cache/error.go diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index 3b790e1b..29bfa63c 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -1,3 +1,18 @@ +// Package cache provides methods for caching CRL +// +// The fileSystemCache is an implementation of the Cache interface that uses the +// file system to store CRLs. The file system cache is built on top of the OS +// file system to leverage the file system's concurrency control and atomicity. +// +// The CRL is stored in a tarball format, which contains two files: base.crl and +// metadata.json. The base.crl file contains the base CRL in DER format, and the +// metadata.json file contains the metadata of the CRL. +// +// To implement a new cache, you need to create a new struct that implements the +// Cache interface. +// +// > Note: Please ensure that the implementation supports *CRL as the +// type of value field to cache a CRL. package cache import ( @@ -8,10 +23,14 @@ import ( type Cache interface { // Get retrieves the content with the given key // - // if the key does not exist, return os.ErrNotExist + // - if the key does not exist, return os.ErrNotExist + // - when request a key of a CRL, the implementation MUST return a *CRL Get(ctx context.Context, key string) (any, error) // Set stores the content with the given key + // + // the implementation MUST support *CRL as the type of value field to + // cache a CRL Set(ctx context.Context, key string, value any) error // Delete removes the content with the given key diff --git a/revocation/crl/cache/blob.go b/revocation/crl/cache/crl.go similarity index 60% rename from revocation/crl/cache/blob.go rename to revocation/crl/cache/crl.go index 85b4b210..7c092408 100644 --- a/revocation/crl/cache/blob.go +++ b/revocation/crl/cache/crl.go @@ -2,10 +2,7 @@ package cache import ( "archive/tar" - "bytes" - "crypto/sha256" "crypto/x509" - "encoding/gob" "encoding/json" "errors" "fmt" @@ -26,8 +23,6 @@ const ( type CRL struct { BaseCRL *x509.RevocationList Metadata Metadata - - checksum [32]byte } // Metadata stores the metadata infomation of the CRL @@ -56,49 +51,34 @@ func NewCRL(baseCRL *x509.RevocationList, url string) (*CRL, error) { return crl, nil } -func (c *CRL) IsCached() bool { - // check c.checksum is empty - checksum, err := c.computCheckSum() - if err != nil { - return false - } - - return c.checksum == checksum -} - -func (c *CRL) computCheckSum() ([32]byte, error) { - toBeHashed := struct { - BaseCRL []byte - Metadata Metadata - }{ - BaseCRL: c.BaseCRL.Raw, - Metadata: c.Metadata, - } - - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - if err := enc.Encode(toBeHashed); err != nil { - return [32]byte{}, err - } - - // sha256 hash - return sha256.Sum256(buf.Bytes()), nil -} - // ParseCRLFromTarball parses the CRL blob from a tarball +// +// The tarball should contain two files: +// - base.crl: the base CRL in DER format +// - metadata.json: the metadata of the CRL +// +// example of metadata.json: +// +// { +// "base.crl": { +// "url": "https://example.com/base.crl", +// "createAt": "2021-09-01T00:00:00Z" +// } +// } func ParseCRLFromTarball(data io.Reader) (*CRL, error) { - crlBlob := &CRL{cached: true} + crl := &CRL{} // parse the tarball tar := tar.NewReader(data) - for { header, err := tar.Next() if err == io.EOF { break } if err != nil { - return nil, err + return nil, &ParseCRLFromTarballError{ + Err: fmt.Errorf("failed to read tarball: %w", err), + } } switch header.Name { @@ -112,37 +92,65 @@ func ParseCRLFromTarball(data io.Reader) (*CRL, error) { var baseCRL *x509.RevocationList baseCRL, err = x509.ParseRevocationList(data) if err != nil { - return nil, err + return nil, &ParseCRLFromTarballError{ + Err: fmt.Errorf("failed to parse base CRL from tarball: %w", err), + } } - crlBlob.BaseCRL = baseCRL + crl.BaseCRL = baseCRL case MetadataFile: // parse metadata var metadata Metadata if err := json.NewDecoder(tar).Decode(&metadata); err != nil { - return nil, err + return nil, &ParseCRLFromTarballError{ + Err: fmt.Errorf("failed to parse CRL metadata from tarball: %w", err), + } } - crlBlob.Metadata = metadata + crl.Metadata = metadata default: - return nil, fmt.Errorf("unknown file in tarball: %s", header.Name) + return nil, &ParseCRLFromTarballError{ + Err: fmt.Errorf("unexpected file in CRL tarball: %s", header.Name), + } } } - if crlBlob.BaseCRL == nil { - return nil, errors.New("base.crl is missing") + // validate + if crl.BaseCRL == nil { + return nil, &ParseCRLFromTarballError{ + Err: errors.New("base CRL is missing from cached tarball"), + } } - - if crlBlob.Metadata.BaseCRL.URL == "" { - return nil, errors.New("base CRL's URL is missing from metadata.json") + if crl.Metadata.BaseCRL.URL == "" { + return nil, &ParseCRLFromTarballError{ + Err: errors.New("base CRL URL is missing from cached tarball"), + } + } + if crl.Metadata.BaseCRL.CreateAt.IsZero() { + return nil, &ParseCRLFromTarballError{ + Err: errors.New("base CRL creation time is missing from cached tarball"), + } } - return crlBlob, nil + return crl, nil } // SaveAsTar saves the CRL blob as a tarball, including the base CRL and // metadata +// +// The tarball should contain two files: +// - base.crl: the base CRL in DER format +// - metadata.json: the metadata of the CRL +// +// example of metadata.json: +// +// { +// "base.crl": { +// "url": "https://example.com/base.crl", +// "createAt": "2021-09-01T00:00:00Z" +// } +// } func SaveAsTarball(w io.Writer, crl *CRL) (err error) { tarWriter := tar.NewWriter(w) // Add base.crl diff --git a/revocation/crl/cache/error.go b/revocation/crl/cache/error.go new file mode 100644 index 00000000..c08c6726 --- /dev/null +++ b/revocation/crl/cache/error.go @@ -0,0 +1,14 @@ +package cache + +// ParseCRLFromTarballError is an error type for when parsing a CRL from +// a tarball +// +// This error indicates that the tarball was broken or required data was +// missing +type ParseCRLFromTarballError struct { + Err error +} + +func (e *ParseCRLFromTarballError) Error() string { + return e.Err.Error() +} diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go index 1d3098a7..54a7f107 100644 --- a/revocation/crl/cache/fs.go +++ b/revocation/crl/cache/fs.go @@ -71,7 +71,7 @@ func (c *fileSystemCache) Set(ctx context.Context, key string, value any) error if err != nil { return err } - if err := crlBlob.SaveAsTarball(tempFile); err != nil { + if err := SaveAsTarball(tempFile, crlBlob); err != nil { return err } From 1dc7d9a933d289fbb5775244e550648aafd61c8b Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 1 Aug 2024 08:54:07 +0000 Subject: [PATCH 051/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/error.go | 14 - revocation/crl/crl.go | 300 --------- revocation/crl/crl_test.go | 675 ------------------- revocation/crl/fetcher.go | 92 --- revocation/{ => internal}/crl/cache/cache.go | 0 revocation/{ => internal}/crl/cache/crl.go | 14 +- revocation/{ => internal}/crl/cache/dummy.go | 0 revocation/internal/crl/cache/error.go | 14 + revocation/{ => internal}/crl/cache/fs.go | 0 revocation/internal/crl/fetch.go | 95 +++ revocation/ocsp/ocsp.go | 14 +- revocation/ocsp/ocsp_test.go | 14 +- revocation/result/errors.go | 23 - revocation/result/errors_test.go | 38 -- revocation/result/results.go | 76 +-- revocation/result/results_test.go | 35 - revocation/revocation.go | 174 +---- revocation/revocation_test.go | 302 +-------- 18 files changed, 153 insertions(+), 1727 deletions(-) delete mode 100644 revocation/crl/cache/error.go delete mode 100644 revocation/crl/crl.go delete mode 100644 revocation/crl/crl_test.go delete mode 100644 revocation/crl/fetcher.go rename revocation/{ => internal}/crl/cache/cache.go (100%) rename revocation/{ => internal}/crl/cache/crl.go (92%) rename revocation/{ => internal}/crl/cache/dummy.go (100%) create mode 100644 revocation/internal/crl/cache/error.go rename revocation/{ => internal}/crl/cache/fs.go (100%) create mode 100644 revocation/internal/crl/fetch.go diff --git a/revocation/crl/cache/error.go b/revocation/crl/cache/error.go deleted file mode 100644 index c08c6726..00000000 --- a/revocation/crl/cache/error.go +++ /dev/null @@ -1,14 +0,0 @@ -package cache - -// ParseCRLFromTarballError is an error type for when parsing a CRL from -// a tarball -// -// This error indicates that the tarball was broken or required data was -// missing -type ParseCRLFromTarballError struct { - Err error -} - -func (e *ParseCRLFromTarballError) Error() string { - return e.Err.Error() -} diff --git a/revocation/crl/crl.go b/revocation/crl/crl.go deleted file mode 100644 index 1ea8d04d..00000000 --- a/revocation/crl/crl.go +++ /dev/null @@ -1,300 +0,0 @@ -// 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 crl provides methods for checking the revocation status of a -// certificate using CRL -package crl - -import ( - "context" - "crypto/x509" - "encoding/asn1" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "sort" - "strings" - "time" - - "github.com/notaryproject/notation-core-go/revocation/crl/cache" - "github.com/notaryproject/notation-core-go/revocation/result" -) - -// oidInvalidityDate is the object identifier for the invalidity date -// CRL entry extension. (See RFC 5280, Section 5.3.2) -var oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} - -// maxCRLSize is the maximum size of CRL in bytes -const maxCRLSize = 10 << 20 // 10 MiB - -// Options specifies values that are needed to check CRL -type Options struct { - // HTTPClient is the HTTP client used to download CRL - HTTPClient *http.Client - - // SigningTime is the time when the certificate's private key is used to - // sign the data. - SigningTime time.Time -} - -// CertCheckStatus checks the revocation status of a certificate using CRL -// -// The function checks the revocation status of the certificate by downloading -// the CRL from the CRL distribution points specified in the certificate. -// -// If the invalidity date extension is present in the CRL entry and SigningTime -// is not zero, the certificate is considered revoked if the SigningTime is -// after the invalidity date. (See RFC 5280, Section 5.3.2) -func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { - if opts.HTTPClient == nil { - return &result.CertRevocationResult{Error: errors.New("invalid input: a non-nil httpClient must be specified")} - } - - if !SupportCRL(cert) { - return &result.CertRevocationResult{Error: errors.New("certificate does not support CRL")} - } - - // Check CRLs - crlResults := make([]*result.CRLResult, len(cert.CRLDistributionPoints)) - for i, crlURL := range cert.CRLDistributionPoints { - baseCRL, err := download(ctx, crlURL, opts.HTTPClient) - if err != nil { - crlResults[i] = &result.CRLResult{ - Error: fmt.Errorf("failed to download CRL from %s: %w", crlURL, err), - } - continue - } - - err = validate(baseCRL, issuer) - if err != nil { - return &result.CertRevocationResult{ - Result: result.ResultUnknown, - CRLResults: crlResults, - Error: err, - } - } - - return checkRevocation(cert, baseCRL, opts.SigningTime) - } - - return &result.CertRevocationResult{ - Result: result.ResultUnknown, - CRLResults: crlResults, - Error: crlResults[len(crlResults)-1].Error} -} - -// SupportCRL checks if the certificate supports CRL. -func SupportCRL(cert *x509.Certificate) bool { - return len(cert.CRLDistributionPoints) > 0 -} - -func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { - // check signature - if err := crl.CheckSignatureFrom(issuer); err != nil { - return fmt.Errorf("CRL signature verification failed: %w", err) - } - - // check validity - if !crl.NextUpdate.IsZero() && time.Now().After(crl.NextUpdate) { - return fmt.Errorf("CRL is expired: %v", crl.NextUpdate) - } - - // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) - for _, ext := range crl.Extensions { - if ext.Critical { - return fmt.Errorf("CRL contains unsupported critical extension: %v", ext.Id) - } - } - - return nil -} - -// checkRevocation checks if the certificate is revoked or not -func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signingTime time.Time) *result.CertRevocationResult { - if cert == nil { - return &result.CertRevocationResult{Error: errors.New("certificate is nil")} - } - - if baseCRL == nil { - return &result.CertRevocationResult{Error: errors.New("CRL is nil")} - } - - // tempRevokedEntries contains revocation entries with reasons such as - // CertificateHold or RemoveFromCRL. - // - // If the certificate is revoked with CertificateHold, it is temporarily - // revoked. If the certificate is shown in the CRL with RemoveFromCRL, - // it is unrevoked. - var tempRevokedEntries []x509.RevocationListEntry - - for _, revocationEntry := range baseCRL.RevokedCertificateEntries { - if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { - extensions, err := parseEntryExtensions(revocationEntry) - if err != nil { - return &result.CertRevocationResult{ - Result: result.ResultUnknown, - CRLResults: []*result.CRLResult{ - { - Error: err, - }, - }, - Error: err, - } - } - - // validate signingTime and invalidityDate - if !signingTime.IsZero() && !extensions.invalidityDate.IsZero() && - signingTime.Before(extensions.invalidityDate) { - // signing time is before the invalidity date which means the - // certificate is not revoked at the time of signing. - continue - } - - if result.CRLReasonCodeCertificateHold.Equal(revocationEntry.ReasonCode) || - result.CRLReasonCodeRemoveFromCRL.Equal(revocationEntry.ReasonCode) { - // temporarily revoked - tempRevokedEntries = append(tempRevokedEntries, revocationEntry) - } else { - // permanently revoked - return &result.CertRevocationResult{ - Result: result.ResultRevoked, - CRLResults: []*result.CRLResult{{ - ReasonCode: result.CRLReasonCode(revocationEntry.ReasonCode), - RevocationTime: revocationEntry.RevocationTime}}, - } - } - } - } - - // check if the revocation with CertificateHold or RemoveFromCRL - if len(tempRevokedEntries) > 0 { - // sort by revocation time (ascending order) - sort.Slice(tempRevokedEntries, func(i, j int) bool { - return tempRevokedEntries[i].RevocationTime.Before(tempRevokedEntries[j].RevocationTime) - }) - - // the revocation status depends on the most recent one - lastEntry := tempRevokedEntries[len(tempRevokedEntries)-1] - if !result.CRLReasonCodeRemoveFromCRL.Equal(lastEntry.ReasonCode) { - return &result.CertRevocationResult{ - Result: result.ResultRevoked, - CRLResults: []*result.CRLResult{{ - ReasonCode: result.CRLReasonCode(lastEntry.ReasonCode), - RevocationTime: lastEntry.RevocationTime}}, - } - } - } - - return &result.CertRevocationResult{ - Result: result.ResultOK, - CRLResults: []*result.CRLResult{}, - } -} - -type entryExtensions struct { - // invalidityDate is the date when the key is invalid. - invalidityDate time.Time -} - -func parseEntryExtensions(entry x509.RevocationListEntry) (entryExtensions, error) { - extensions := entryExtensions{} - for _, ext := range entry.ExtraExtensions { - switch { - case ext.Id.Equal(oidInvalidityDate): - var invalidityDate time.Time - rest, err := asn1.UnmarshalWithParams(ext.Value, &invalidityDate, "generalized") - if err != nil { - return entryExtensions{}, fmt.Errorf("failed to parse invalidity date: %w", err) - } - if len(rest) > 0 { - return entryExtensions{}, fmt.Errorf("invalid invalidity date extension") - } - - extensions.invalidityDate = invalidityDate - default: - if ext.Critical { - // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) - return entryExtensions{}, fmt.Errorf("CRL entry contains unsupported critical extension: %v", ext.Id) - } - } - } - - return extensions, nil -} - -func fetchCRL(ctx context.Context, cacheClient cache.Cache, crlURL string, client *http.Client) (*cache.CRL, error) { - // check cache - // try to get from cache - obj, err := cacheClient.Get(ctx, tarStoreName(crlURL)) - if err != nil { - if os.IsNotExist(err) { - crl, err := download(ctx, crlURL, client) - if err != nil { - return nil, err - } - - return cache.NewCRL(crl, crlURL), nil - } - - return nil, err - } - - crl, ok := obj.(*cache.CRL) - if !ok { - return nil, fmt.Errorf("invalid cache object type: %T", obj) - } - - return crl, nil -} - -func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { - // validate URL - parsedURL, err := url.Parse(crlURL) - if err != nil { - return nil, fmt.Errorf("invalid CRL URL: %w", err) - } - if strings.ToLower(parsedURL.Scheme) != "http" { - return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme) - } - - // download CRL - req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to create CRL request: %w", err) - } - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - // check response - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) - } - - // read with size limit - limitedReader := io.LimitReader(resp.Body, maxCRLSize) - data, err := io.ReadAll(limitedReader) - if err != nil { - return nil, fmt.Errorf("failed to read CRL response: %w", err) - } - if len(data) == maxCRLSize { - return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize) - } - - return x509.ParseRevocationList(data) -} diff --git a/revocation/crl/crl_test.go b/revocation/crl/crl_test.go deleted file mode 100644 index 854c3d99..00000000 --- a/revocation/crl/crl_test.go +++ /dev/null @@ -1,675 +0,0 @@ -// 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 crl - -import ( - "bytes" - "context" - "crypto/rand" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "fmt" - "io" - "math/big" - "net/http" - "testing" - "time" - - "github.com/notaryproject/notation-core-go/revocation/result" - "github.com/notaryproject/notation-core-go/testhelper" -) - -func TestCertCheckStatus(t *testing.T) { - t.Run("http client is nil", func(t *testing.T) { - r := CertCheckStatus(context.Background(), &x509.Certificate{}, &x509.Certificate{}, Options{}) - if r.Error == nil { - t.Fatal("expected error") - } - }) - - t.Run("certificate does not support CRL", func(t *testing.T) { - r := CertCheckStatus(context.Background(), &x509.Certificate{}, &x509.Certificate{}, Options{ - HTTPClient: http.DefaultClient, - }) - if r.Error == nil { - t.Fatal("expected error") - } - }) - - t.Run("download error", func(t *testing.T) { - cert := &x509.Certificate{ - CRLDistributionPoints: []string{"http://example.com"}, - } - r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, Options{ - HTTPClient: &http.Client{ - Transport: errorRoundTripperMock{}, - }, - }) - if r.Error == nil { - t.Fatal("expected error") - } - }) - - t.Run("CRL validate failed", func(t *testing.T) { - cert := &x509.Certificate{ - CRLDistributionPoints: []string{"http://example.com"}, - } - r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, Options{ - HTTPClient: &http.Client{ - Transport: expiredCRLRoundTripperMock{}, - }, - }) - if r.Error == nil { - t.Fatal("expected error") - } - }) - - t.Run("revoked", func(t *testing.T) { - chain := testhelper.GetRevokableRSAChain(2, false, true) - issuerCert := chain[1].Cert - issuerKey := chain[1].PrivateKey - - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - NextUpdate: time.Now().Add(time.Hour), - Number: big.NewInt(20240720), - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: chain[0].Cert.SerialNumber, - RevocationTime: time.Now().Add(-time.Hour), - ReasonCode: int(result.CRLReasonCodeUnspecified), - }, - }, - }, issuerCert, issuerKey) - if err != nil { - t.Fatal(err) - } - - r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, Options{ - HTTPClient: &http.Client{ - Transport: expectedRoundTripperMock{Body: crlBytes}, - }, - }) - if r.Result != result.ResultRevoked { - t.Fatalf("expected revoked, got %s", r.Result) - } - - }) -} - -func TestValidate(t *testing.T) { - t.Run("expired CRL", func(t *testing.T) { - chain := testhelper.GetRevokableRSAChain(1, false, true) - issuerCert := chain[0].Cert - issuerKey := chain[0].PrivateKey - - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - NextUpdate: time.Now().Add(-time.Hour), - Number: big.NewInt(20240720), - }, issuerCert, issuerKey) - if err != nil { - t.Fatal(err) - } - - crl, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatal(err) - } - - if err := validate(crl, issuerCert); err == nil { - t.Fatal("expected error") - } - }) - - t.Run("check signature failed", func(t *testing.T) { - crl := &x509.RevocationList{ - NextUpdate: time.Now().Add(time.Hour), - } - - if err := validate(crl, &x509.Certificate{}); err == nil { - t.Fatal("expected error") - } - }) - - t.Run("unsupported CRL critical extensions", func(t *testing.T) { - chain := testhelper.GetRevokableRSAChain(1, false, true) - issuerCert := chain[0].Cert - issuerKey := chain[0].PrivateKey - - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - NextUpdate: time.Now().Add(time.Hour), - Number: big.NewInt(20240720), - }, issuerCert, issuerKey) - if err != nil { - t.Fatal(err) - } - - crl, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatal(err) - } - - // add unsupported critical extension - crl.Extensions = []pkix.Extension{ - { - Id: []int{1, 2, 3}, - Critical: true, - }, - } - - if err := validate(crl, issuerCert); err == nil { - t.Fatal("expected error") - } - }) -} - -func TestCheckRevocation(t *testing.T) { - cert := &x509.Certificate{ - SerialNumber: big.NewInt(1), - } - signingTime := time.Now() - - t.Run("certificate is nil", func(t *testing.T) { - r := checkRevocation(nil, &x509.RevocationList{}, signingTime) - if r.Error == nil { - t.Fatal("expected error") - } - }) - - t.Run("CRL is nil", func(t *testing.T) { - r := checkRevocation(cert, nil, signingTime) - if r.Error == nil { - t.Fatal("expected error") - } - }) - - t.Run("not revoked", func(t *testing.T) { - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(2), - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultOK { - t.Fatalf("unexpected result, got %s", r.Result) - } - }) - - t.Run("revoked", func(t *testing.T) { - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(-time.Hour), - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultRevoked { - t.Fatalf("expected revoked, got %s", r.Result) - } - }) - - t.Run("revoked but signing time is before invalidityDate", func(t *testing.T) { - invalidityDate := time.Now().Add(time.Hour) - invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) - if err != nil { - t.Fatal(err) - } - - extensions := []pkix.Extension{ - { - Id: oidInvalidityDate, - Critical: false, - Value: invalidityDateBytes, - }, - } - - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(time.Hour), - ExtraExtensions: extensions, - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultOK { - t.Fatalf("unexpected result, got %s", r.Result) - } - }) - - t.Run("revoked; signing time is after invalidityDate", func(t *testing.T) { - invalidityDate := time.Now().Add(-time.Hour) - invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) - if err != nil { - t.Fatal(err) - } - - extensions := []pkix.Extension{ - { - Id: oidInvalidityDate, - Critical: false, - Value: invalidityDateBytes, - }, - } - - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(-time.Hour), - ExtraExtensions: extensions, - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultRevoked { - t.Fatalf("expected revoked, got %s", r.Result) - } - }) - - t.Run("revoked and signing time is zero", func(t *testing.T) { - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Time{}, - }, - }, - } - r := checkRevocation(cert, baseCRL, time.Time{}) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultRevoked { - t.Fatalf("expected revoked, got %s", r.Result) - } - }) - - t.Run("revoked but not permanently", func(t *testing.T) { - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(-time.Hour), - }, - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), - RevocationTime: time.Now().Add(-time.Minute * 10), - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultOK { - t.Fatalf("unexpected result, got %s", r.Result) - } - }) - - t.Run("revoked but not permanently with disordered entry list", func(t *testing.T) { - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), - RevocationTime: time.Now().Add(-time.Minute * 10), - }, - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(-time.Hour), - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultOK { - t.Fatalf("unexpected result, got %s", r.Result) - } - }) - - t.Run("RemoveFromCRL before CertificateHold", func(t *testing.T) { - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), - RevocationTime: time.Now().Add(-time.Hour), - }, - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(-time.Minute * 10), - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultRevoked { - t.Fatalf("expected revoked, got %s", r.Result) - } - }) - - t.Run("multiple CertificateHold with RemoveFromCRL and disordered entry list", func(t *testing.T) { - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(-time.Minute * 20), - }, - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), - RevocationTime: time.Now().Add(-time.Hour), - }, - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeCertificateHold), - RevocationTime: time.Now().Add(-time.Minute * 50), - }, - { - SerialNumber: big.NewInt(1), - ReasonCode: int(result.CRLReasonCodeRemoveFromCRL), - RevocationTime: time.Now().Add(-time.Minute * 40), - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error != nil { - t.Fatal(r.Error) - } - if r.Result != result.ResultRevoked { - t.Fatalf("expected revoked, got %s", r.Result) - } - }) - - t.Run("revocation entry validation error", func(t *testing.T) { - baseCRL := &x509.RevocationList{ - RevokedCertificateEntries: []x509.RevocationListEntry{ - { - SerialNumber: big.NewInt(1), - ExtraExtensions: []pkix.Extension{ - { - Id: []int{1, 2, 3}, - Critical: true, - }, - }, - }, - }, - } - r := checkRevocation(cert, baseCRL, signingTime) - if r.Error == nil { - t.Fatal("expected error") - } - }) -} - -func TestParseEntryExtension(t *testing.T) { - t.Run("unsupported critical extension", func(t *testing.T) { - entry := x509.RevocationListEntry{ - ExtraExtensions: []pkix.Extension{ - { - Id: []int{1, 2, 3}, - Critical: true, - }, - }, - } - if _, err := parseEntryExtensions(entry); err == nil { - t.Fatal("expected error") - } - }) - - t.Run("valid extension", func(t *testing.T) { - entry := x509.RevocationListEntry{ - ExtraExtensions: []pkix.Extension{ - { - Id: []int{1, 2, 3}, - Critical: false, - }, - }, - } - if _, err := parseEntryExtensions(entry); err != nil { - t.Fatal(err) - } - }) - - t.Run("parse invalidityDate", func(t *testing.T) { - - // create a time and marshal it to be generalizedTime - invalidityDate := time.Now() - invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) - if err != nil { - t.Fatal(err) - } - - entry := x509.RevocationListEntry{ - ExtraExtensions: []pkix.Extension{ - { - Id: oidInvalidityDate, - Critical: false, - Value: invalidityDateBytes, - }, - }, - } - extensions, err := parseEntryExtensions(entry) - if err != nil { - t.Fatal(err) - } - - if extensions.invalidityDate.IsZero() { - t.Fatal("expected invalidityDate") - } - }) - - t.Run("parse invalidityDate with error", func(t *testing.T) { - // invalid invalidityDate extension - entry := x509.RevocationListEntry{ - ExtraExtensions: []pkix.Extension{ - { - Id: oidInvalidityDate, - Critical: false, - Value: []byte{0x00, 0x01, 0x02, 0x03}, - }, - }, - } - _, err := parseEntryExtensions(entry) - if err == nil { - t.Fatal("expected error") - } - - // invalidityDate extension with extra bytes - invalidityDate := time.Now() - invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) - if err != nil { - t.Fatal(err) - } - invalidityDateBytes = append(invalidityDateBytes, 0x00) - - entry = x509.RevocationListEntry{ - ExtraExtensions: []pkix.Extension{ - { - Id: oidInvalidityDate, - Critical: false, - Value: invalidityDateBytes, - }, - }, - } - _, err = parseEntryExtensions(entry) - if err == nil { - t.Fatal("expected error") - } - }) -} - -// marshalGeneralizedTimeToBytes converts a time.Time to ASN.1 GeneralizedTime bytes. -func marshalGeneralizedTimeToBytes(t time.Time) ([]byte, error) { - // ASN.1 GeneralizedTime requires the time to be in UTC - t = t.UTC() - // Use asn1.Marshal to directly get the ASN.1 GeneralizedTime bytes - return asn1.Marshal(t) -} - -func TestDownload(t *testing.T) { - t.Run("parse url error", func(t *testing.T) { - _, err := download(context.Background(), ":", http.DefaultClient) - if err == nil { - t.Fatal("expected error") - } - }) - t.Run("https download", func(t *testing.T) { - _, err := download(context.Background(), "https://example.com", http.DefaultClient) - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("http.NewRequestWithContext error", func(t *testing.T) { - var ctx context.Context = nil - _, err := download(ctx, "http://example.com", &http.Client{}) - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("client.Do error", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: errorRoundTripperMock{}, - }) - - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("status code is not 2xx", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: serverErrorRoundTripperMock{}, - }) - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("readAll error", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: readFailedRoundTripperMock{}, - }) - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("exceed the size limit", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, - }) - if err == nil { - t.Fatal("expected error") - } - }) -} - -type errorRoundTripperMock struct{} - -func (rt errorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { - return nil, fmt.Errorf("error") -} - -type serverErrorRoundTripperMock struct{} - -func (rt serverErrorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusInternalServerError, - }, nil -} - -type readFailedRoundTripperMock struct{} - -func (rt readFailedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: errorReaderMock{}, - }, nil -} - -type expiredCRLRoundTripperMock struct{} - -func (rt expiredCRLRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { - chain := testhelper.GetRevokableRSAChain(1, false, true) - issuerCert := chain[0].Cert - issuerKey := chain[0].PrivateKey - - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - NextUpdate: time.Now().Add(-time.Hour), - Number: big.NewInt(20240720), - }, issuerCert, issuerKey) - if err != nil { - return nil, err - } - - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewBuffer(crlBytes)), - }, nil -} - -type errorReaderMock struct{} - -func (r errorReaderMock) Read(p []byte) (n int, err error) { - return 0, fmt.Errorf("error") -} - -func (r errorReaderMock) Close() error { - return nil -} - -type expectedRoundTripperMock struct { - Body []byte -} - -func (rt expectedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewBuffer(rt.Body)), - }, nil -} diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go deleted file mode 100644 index beb738ad..00000000 --- a/revocation/crl/fetcher.go +++ /dev/null @@ -1,92 +0,0 @@ -package crl - -import ( - "context" - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "fmt" - "io" - "net/http" - "os" - - "github.com/notaryproject/notation-core-go/revocation/crl/cache" -) - -// Fetcher is an interface to fetch CRL -type Fetcher interface { - // Fetch retrieves the CRL with the given URL - Fetch(ctx context.Context, url string) (crl *cache.CRL, err error) -} - -// cachedFetcher is a CRL fetcher with cache -type cachedFetcher struct { - httpClient *http.Client - cache cache.Cache -} - -// NewCachedFetcher creates a new CRL fetcher with cache -func NewCachedFetcher(httpClient *http.Client, cache cache.Cache) Fetcher { - return &cachedFetcher{ - httpClient: httpClient, - cache: cache, - } -} - -// Fetch retrieves the CRL with the given URL -func (c *cachedFetcher) Fetch(ctx context.Context, url string) (crl *cache.CRL, err error) { - fmt.Println("fetching CRL from", url) - // try to get from cache - obj, err := c.cache.Get(ctx, tarStoreName(url)) - if err != nil { - if os.IsNotExist(err) { - // fallback to fetch from remote - crlStore, err := c.download(url) - if err != nil { - return nil, err - } - return crlStore, nil - } - - return nil, err - } - - crl, ok := obj.(*cache.CRL) - if !ok { - return nil, fmt.Errorf("invalid cache object type: %T", obj) - } - - return crl, nil -} - -func (c *cachedFetcher) download(url string) (*cache.CRL, error) { - // fetch from remote - resp, err := c.httpClient.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - crl, err := x509.ParseRevocationList(data) - if err != nil { - return nil, err - } - - crlStore := cache.NewCRL(crl, url) - return crlStore, nil -} - -func tarStoreName(url string) string { - return hashURL(url) + ".tar" -} - -// hashURL hashes the URL with SHA256 and returns the hex-encoded result -func hashURL(url string) string { - hash := sha256.Sum256([]byte(url)) - return hex.EncodeToString(hash[:]) -} diff --git a/revocation/crl/cache/cache.go b/revocation/internal/crl/cache/cache.go similarity index 100% rename from revocation/crl/cache/cache.go rename to revocation/internal/crl/cache/cache.go diff --git a/revocation/crl/cache/crl.go b/revocation/internal/crl/cache/crl.go similarity index 92% rename from revocation/crl/cache/crl.go rename to revocation/internal/crl/cache/crl.go index 7c092408..8420a95d 100644 --- a/revocation/crl/cache/crl.go +++ b/revocation/internal/crl/cache/crl.go @@ -76,7 +76,7 @@ func ParseCRLFromTarball(data io.Reader) (*CRL, error) { break } if err != nil { - return nil, &ParseCRLFromTarballError{ + return nil, &BrokenFileError{ Err: fmt.Errorf("failed to read tarball: %w", err), } } @@ -92,7 +92,7 @@ func ParseCRLFromTarball(data io.Reader) (*CRL, error) { var baseCRL *x509.RevocationList baseCRL, err = x509.ParseRevocationList(data) if err != nil { - return nil, &ParseCRLFromTarballError{ + return nil, &BrokenFileError{ Err: fmt.Errorf("failed to parse base CRL from tarball: %w", err), } } @@ -102,7 +102,7 @@ func ParseCRLFromTarball(data io.Reader) (*CRL, error) { // parse metadata var metadata Metadata if err := json.NewDecoder(tar).Decode(&metadata); err != nil { - return nil, &ParseCRLFromTarballError{ + return nil, &BrokenFileError{ Err: fmt.Errorf("failed to parse CRL metadata from tarball: %w", err), } } @@ -110,7 +110,7 @@ func ParseCRLFromTarball(data io.Reader) (*CRL, error) { crl.Metadata = metadata default: - return nil, &ParseCRLFromTarballError{ + return nil, &BrokenFileError{ Err: fmt.Errorf("unexpected file in CRL tarball: %s", header.Name), } } @@ -118,17 +118,17 @@ func ParseCRLFromTarball(data io.Reader) (*CRL, error) { // validate if crl.BaseCRL == nil { - return nil, &ParseCRLFromTarballError{ + return nil, &BrokenFileError{ Err: errors.New("base CRL is missing from cached tarball"), } } if crl.Metadata.BaseCRL.URL == "" { - return nil, &ParseCRLFromTarballError{ + return nil, &BrokenFileError{ Err: errors.New("base CRL URL is missing from cached tarball"), } } if crl.Metadata.BaseCRL.CreateAt.IsZero() { - return nil, &ParseCRLFromTarballError{ + return nil, &BrokenFileError{ Err: errors.New("base CRL creation time is missing from cached tarball"), } } diff --git a/revocation/crl/cache/dummy.go b/revocation/internal/crl/cache/dummy.go similarity index 100% rename from revocation/crl/cache/dummy.go rename to revocation/internal/crl/cache/dummy.go diff --git a/revocation/internal/crl/cache/error.go b/revocation/internal/crl/cache/error.go new file mode 100644 index 00000000..fb3853a1 --- /dev/null +++ b/revocation/internal/crl/cache/error.go @@ -0,0 +1,14 @@ +package cache + +// BrokenFileError is an error type for when parsing a CRL from +// a tarball +// +// This error indicates that the tarball was broken or required data was +// missing +type BrokenFileError struct { + Err error +} + +func (e *BrokenFileError) Error() string { + return e.Err.Error() +} diff --git a/revocation/crl/cache/fs.go b/revocation/internal/crl/cache/fs.go similarity index 100% rename from revocation/crl/cache/fs.go rename to revocation/internal/crl/cache/fs.go diff --git a/revocation/internal/crl/fetch.go b/revocation/internal/crl/fetch.go new file mode 100644 index 00000000..39fc9e68 --- /dev/null +++ b/revocation/internal/crl/fetch.go @@ -0,0 +1,95 @@ +package crl + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + + "github.com/notaryproject/notation-core-go/revocation/internal/crl/cache" +) + +// maxCRLSize is the maximum size of CRL in bytes +const maxCRLSize = 10 << 20 // 10 MiB + +func fetch(ctx context.Context, cacheClient cache.Cache, crlURL string, client *http.Client) (*cache.CRL, error) { + // check cache + // try to get from cache + obj, err := cacheClient.Get(ctx, tarStoreName(crlURL)) + if err != nil { + var cacheBrokenError cache.BrokenFileError + if os.IsNotExist(err) || errors.As(err, &cacheBrokenError) { + crl, err := download(ctx, crlURL, client) + if err != nil { + return nil, err + } + + return cache.NewCRL(crl, crlURL) + } + + return nil, err + } + + crl, ok := obj.(*cache.CRL) + if !ok { + return nil, fmt.Errorf("invalid cache object type: %T", obj) + } + + return crl, nil +} + +func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { + // validate URL + parsedURL, err := url.Parse(crlURL) + if err != nil { + return nil, fmt.Errorf("invalid CRL URL: %w", err) + } + if strings.ToLower(parsedURL.Scheme) != "http" { + return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme) + } + + // download CRL + req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create CRL request: %w", err) + } + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + // check response + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) + } + + // read with size limit + limitedReader := io.LimitReader(resp.Body, maxCRLSize) + data, err := io.ReadAll(limitedReader) + if err != nil { + return nil, fmt.Errorf("failed to read CRL response: %w", err) + } + if len(data) == maxCRLSize { + return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize) + } + + return x509.ParseRevocationList(data) +} + +func tarStoreName(url string) string { + return hashURL(url) + ".tar" +} + +// hashURL hashes the URL with SHA256 and returns the hex-encoded result +func hashURL(url string) string { + hash := sha256.Sum256([]byte(url)) + return hex.EncodeToString(hash[:]) +} diff --git a/revocation/ocsp/ocsp.go b/revocation/ocsp/ocsp.go index e6996d53..359ce7e9 100644 --- a/revocation/ocsp/ocsp.go +++ b/revocation/ocsp/ocsp.go @@ -99,7 +99,7 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { // Assume cert chain is accurate and next cert in chain is the issuer go func(i int, cert *x509.Certificate) { defer wg.Done() - certResults[i] = CertCheckStatus(cert, opts.CertChain[i+1], opts) + certResults[i] = certCheckStatus(cert, opts.CertChain[i+1], opts) }(i, cert) } // Last is root cert, which will never be revoked by OCSP @@ -115,16 +115,15 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { return certResults, nil } -// CertCheckStatus checks the revocation status of a certificate using OCSP -func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { - if !SupportOCSP(cert) { +func certCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { + ocspURLs := cert.OCSPServer + if len(ocspURLs) == 0 { // OCSP not enabled for this certificate. return &result.CertRevocationResult{ Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})}, } } - ocspURLs := cert.OCSPServer serverResults := make([]*result.ServerResult, len(ocspURLs)) for serverIndex, server := range ocspURLs { @@ -142,11 +141,6 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertR return serverResultsToCertRevocationResult(serverResults) } -// SupportOCSP returns true if the certificate supports OCSP. -func SupportOCSP(cert *x509.Certificate) bool { - return len(cert.OCSPServer) > 0 -} - func checkStatusFromServer(cert, issuer *x509.Certificate, server string, opts Options) *result.ServerResult { // Check valid server if serverURL, err := url.Parse(server); err != nil || !strings.EqualFold(serverURL.Scheme, "http") { diff --git a/revocation/ocsp/ocsp_test.go b/revocation/ocsp/ocsp_test.go index d70922bc..14d43df6 100644 --- a/revocation/ocsp/ocsp_test.go +++ b/revocation/ocsp/ocsp_test.go @@ -93,7 +93,7 @@ func TestCheckStatus(t *testing.T) { HTTPClient: client, } - certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) + certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) }) @@ -105,7 +105,7 @@ func TestCheckStatus(t *testing.T) { HTTPClient: client, } - certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) + certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{{ Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ @@ -122,7 +122,7 @@ func TestCheckStatus(t *testing.T) { HTTPClient: client, } - certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) + certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{{ Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ @@ -140,7 +140,7 @@ func TestCheckStatus(t *testing.T) { HTTPClient: client, } - certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) + certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) }) @@ -203,7 +203,7 @@ func TestCheckStatusForNonSelfSignedSingleCert(t *testing.T) { func TestCheckStatusForChain(t *testing.T) { zeroTime := time.Time{} - testChain := testhelper.GetRevokableRSAChain(6, true, false) + testChain := testhelper.GetRevokableRSAChain(6) revokableChain := make([]*x509.Certificate, 6) for i, tuple := range testChain { revokableChain[i] = tuple.Cert @@ -457,7 +457,7 @@ func TestCheckStatusErrors(t *testing.T) { rootCertTuple := testhelper.GetRSARootCertificate() noOCSPChain := []*x509.Certificate{leafCertTuple.Cert, rootCertTuple.Cert} - revokableTuples := testhelper.GetRevokableRSAChain(3, true, false) + revokableTuples := testhelper.GetRevokableRSAChain(3) noRootChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert} backwardsChain := []*x509.Certificate{revokableTuples[2].Cert, revokableTuples[1].Cert, revokableTuples[0].Cert} okChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert, revokableTuples[2].Cert} @@ -663,7 +663,7 @@ func TestCheckStatusErrors(t *testing.T) { } func TestCheckOCSPInvalidChain(t *testing.T) { - revokableTuples := testhelper.GetRevokableRSAChain(4, true, false) + revokableTuples := testhelper.GetRevokableRSAChain(4) misorderedIntermediateTuples := []testhelper.RSACertTuple{revokableTuples[1], revokableTuples[0], revokableTuples[2], revokableTuples[3]} misorderedIntermediateChain := []*x509.Certificate{revokableTuples[1].Cert, revokableTuples[0].Cert, revokableTuples[2].Cert, revokableTuples[3].Cert} for i, cert := range misorderedIntermediateChain { diff --git a/revocation/result/errors.go b/revocation/result/errors.go index eb821eef..93641998 100644 --- a/revocation/result/errors.go +++ b/revocation/result/errors.go @@ -31,26 +31,3 @@ func (e InvalidChainError) Error() string { } return msg } - -// OCSPFallbackErro is returned when the OCSP check result is of unknown status -// and falls back to CRL -type OCSPFallbackError struct { - // OCSPErr is the error that occurred during the OCSP check - OCSPErr error - - // CRLErr is the error that occurred during the CRL check - CRLErr error -} - -func (e OCSPFallbackError) Error() string { - msg := "the OCSP check result is of unknown status; fallback to CRL" - if e.OCSPErr != nil { - msg += fmt.Sprintf("; OCSP error: %v", e.OCSPErr) - } - - if e.CRLErr != nil { - msg += fmt.Sprintf("; CRL error: %v", e.CRLErr) - } - - return msg -} diff --git a/revocation/result/errors_test.go b/revocation/result/errors_test.go index a406c111..59b47c7b 100644 --- a/revocation/result/errors_test.go +++ b/revocation/result/errors_test.go @@ -37,41 +37,3 @@ func TestInvalidChainError(t *testing.T) { } }) } - -func TestOCSPFallbackError(t *testing.T) { - t.Run("without_inner_error", func(t *testing.T) { - err := &OCSPFallbackError{} - expectedMsg := "the OCSP check result is of unknown status; fallback to CRL" - - if err.Error() != expectedMsg { - t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) - } - }) - - t.Run("with_ocsp_error", func(t *testing.T) { - err := &OCSPFallbackError{OCSPErr: errors.New("ocsp error")} - expectedMsg := "the OCSP check result is of unknown status; fallback to CRL; OCSP error: ocsp error" - - if err.Error() != expectedMsg { - t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) - } - }) - - t.Run("with_crl_error", func(t *testing.T) { - err := &OCSPFallbackError{CRLErr: errors.New("crl error")} - expectedMsg := "the OCSP check result is of unknown status; fallback to CRL; CRL error: crl error" - - if err.Error() != expectedMsg { - t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) - } - }) - - t.Run("with_both_errors", func(t *testing.T) { - err := &OCSPFallbackError{OCSPErr: errors.New("ocsp error"), CRLErr: errors.New("crl error")} - expectedMsg := "the OCSP check result is of unknown status; fallback to CRL; OCSP error: ocsp error; CRL error: crl error" - - if err.Error() != expectedMsg { - t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) - } - }) -} diff --git a/revocation/result/results.go b/revocation/result/results.go index 8dfa5651..c7ecba51 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -14,11 +14,7 @@ // Package result provides general objects that are used across revocation package result -import ( - "fmt" - "strconv" - "time" -) +import "strconv" // Result is a type of enumerated value to help characterize errors. It can be // OK, Unknown, or Revoked @@ -56,7 +52,7 @@ func (r Result) String() string { } } -// ServerResult encapsulates the OCSP result for a single server for a single +// ServerResult encapsulates the result for a single server for a single // certificate in the chain type ServerResult struct { // Result of revocation for this server (Unknown if there is an error which @@ -83,68 +79,6 @@ func NewServerResult(result Result, server string, err error) *ServerResult { } } -// CRLReasonCode is CRL reason code (See RFC 5280, section 5.3.1) -type CRLReasonCode int - -const ( - CRLReasonCodeUnspecified CRLReasonCode = iota - CRLReasonCodeKeyCompromise - CRLReasonCodeCACompromise - CRLReasonCodeAffiliationChanged - CRLReasonCodeSuperseded - CRLReasonCodeCessationOfOperation - CRLReasonCodeCertificateHold - // value 7 is not used - CRLReasonCodeRemoveFromCRL CRLReasonCode = iota + 1 - CRLReasonCodePrivilegeWithdrawn - CRLReasonCodeAACompromise -) - -// Equal checks if the reason code is equal to the given reason code -func (r CRLReasonCode) Equal(reasonCode int) bool { - return int(r) == reasonCode -} - -// String provides a conversion from a ReasonCode to a string -func (r CRLReasonCode) String() string { - switch r { - case CRLReasonCodeUnspecified: - return "Unspecified" - case CRLReasonCodeKeyCompromise: - return "KeyCompromise" - case CRLReasonCodeCACompromise: - return "CACompromise" - case CRLReasonCodeAffiliationChanged: - return "AffiliationChanged" - case CRLReasonCodeSuperseded: - return "Superseded" - case CRLReasonCodeCessationOfOperation: - return "CessationOfOperation" - case CRLReasonCodeCertificateHold: - return "CertificateHold" - case CRLReasonCodeRemoveFromCRL: - return "RemoveFromCRL" - case CRLReasonCodePrivilegeWithdrawn: - return "PrivilegeWithdrawn" - case CRLReasonCodeAACompromise: - return "AACompromise" - default: - return fmt.Sprintf("invalid reason code with value: %d", r) - } -} - -// CRLResult encapsulates the result of a CRL check -type CRLResult struct { - // ReasonCode is the reason code for the CRL status - ReasonCode CRLReasonCode - - // RevocationTime is the time at which the certificate was revoked - RevocationTime time.Time - - // Error is set if there is an error associated with the revocation check - Error error -} - // CertRevocationResult encapsulates the result for a single certificate in the // chain as well as the results from individual servers associated with this // certificate @@ -166,10 +100,4 @@ type CertRevocationResult struct { // Otherwise, every server specified had some error that prevented the // status from being retrieved. These are all contained here for evaluation ServerResults []*ServerResult - - // CRLResults is the result of the CRL check for this certificate - CRLResults []*CRLResult - - // Error is set if there is an error associated with the revocation check - Error error } diff --git a/revocation/result/results_test.go b/revocation/result/results_test.go index a18ef2bf..1c5a503a 100644 --- a/revocation/result/results_test.go +++ b/revocation/result/results_test.go @@ -63,38 +63,3 @@ func TestNewServerResult(t *testing.T) { t.Errorf("Expected %v but got %v", expectedR.Error, r.Error) } } - -func TestCRLReasonCode(t *testing.T) { - expected := []struct { - code int - reason string - }{ - {0, "Unspecified"}, - {1, "KeyCompromise"}, - {2, "CACompromise"}, - {3, "AffiliationChanged"}, - {4, "Superseded"}, - {5, "CessationOfOperation"}, - {6, "CertificateHold"}, - {8, "RemoveFromCRL"}, - {9, "PrivilegeWithdrawn"}, - {10, "AACompromise"}, - } - - for _, e := range expected { - reasonCode := CRLReasonCode(e.code) - if !reasonCode.Equal(e.code) || reasonCode.String() != e.reason { - t.Errorf("Expected %s but got %s", e.reason, CRLReasonCode(e.code).String()) - } - } -} - -func TestInvalidCRLReasonCode(t *testing.T) { - if CRLReasonCode(7).String() != "invalid reason code with value: 7" { - t.Errorf("Expected %s but got %s", "invalid reason code with value: 7", CRLReasonCode(7).String()) - } - - if CRLReasonCode(11).String() != "invalid reason code with value: 11" { - t.Errorf("Expected %s but got %s", "invalid reason code with value: 11", CRLReasonCode(11).String()) - } -} diff --git a/revocation/revocation.go b/revocation/revocation.go index ad18a716..801a0780 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -16,46 +16,26 @@ package revocation import ( - "context" "crypto/x509" "errors" - "fmt" "net/http" - "sync" "time" - "github.com/notaryproject/notation-core-go/revocation/crl" "github.com/notaryproject/notation-core-go/revocation/ocsp" "github.com/notaryproject/notation-core-go/revocation/result" - coreX509 "github.com/notaryproject/notation-core-go/x509" ) // Revocation is an interface that specifies methods used for revocation checking type Revocation interface { // Validate checks the revocation status for a certificate chain using OCSP - // and CRL if OCSP is not available. It returns an array of - // CertRevocationResults that contain the results and any errors that are - // encountered during the process + // and returns an array of CertRevocationResults that contain the results + // and any errors that are encountered during the process Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) } -// Options specifies values that are needed to check revocation -type Options struct { - // OCSPHTTPClient is a required HTTP client for OCSP request - OCSPHTTPClient *http.Client - - // CRLHTTPClient is a required HTTP client for CRL request - CRLHTTPClient *http.Client - - // CertChainPurpose is the purpose of the certificate chain - CertChainPurpose ocsp.Purpose -} - // revocation is an internal struct used for revocation checking type revocation struct { - ctx context.Context - ocspHTTPClient *http.Client - crlHTTPClient *http.Client + httpClient *http.Client certChainPurpose ocsp.Purpose } @@ -64,12 +44,10 @@ func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } - - return NewWithOptions(context.Background(), &Options{ - OCSPHTTPClient: httpClient, - CRLHTTPClient: httpClient, - CertChainPurpose: ocsp.PurposeCodeSigning, - }) + return &revocation{ + httpClient: httpClient, + certChainPurpose: ocsp.PurposeCodeSigning, + }, nil } // NewTimestamp contructs a revocation object for timestamping certificate @@ -78,140 +56,26 @@ func NewTimestamp(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } - - return NewWithOptions(context.Background(), &Options{ - OCSPHTTPClient: httpClient, - CRLHTTPClient: httpClient, - CertChainPurpose: ocsp.PurposeTimestamping, - }) -} - -// NewWithOptions constructs a revocation object with the specified options -func NewWithOptions(ctx context.Context, opts *Options) (Revocation, error) { - if ctx == nil { - return nil, errors.New("invalid input: a non-nil context must be specified") - } - - if opts.OCSPHTTPClient == nil { - return nil, errors.New("invalid input: a non-nil OCSPHTTPClient must be specified") - } - - if opts.CRLHTTPClient == nil { - return nil, errors.New("invalid input: a non-nil CRLHTTPClient must be specified") - } - - switch opts.CertChainPurpose { - case ocsp.PurposeCodeSigning, ocsp.PurposeTimestamping: - default: - return nil, fmt.Errorf("unknown certificate chain purpose %v", opts.CertChainPurpose) - } - return &revocation{ - ctx: ctx, - ocspHTTPClient: opts.OCSPHTTPClient, - crlHTTPClient: opts.CRLHTTPClient, - certChainPurpose: opts.CertChainPurpose, + httpClient: httpClient, + certChainPurpose: ocsp.PurposeTimestamping, }, nil } // Validate checks the revocation status for a certificate chain using OCSP and -// CRL if OCSP is not available. It returns an array of CertRevocationResults -// that contain the results and any errors that are encountered during the -// process. -// -// The certificate chain is expected to be in the order of leaf to root. -// -// This function tries OCSP and falls back to CRL when: -// - OCSP is not supported by the certificate -// - OCSP returns an unknown status +// returns an array of CertRevocationResults that contain the results and any +// errors that are encountered during the process // -// When OCSP returns an unknown status, the function will try to check the -// certificate status using CRL and return certificate result with an -// result.OCSPFallbackError. +// TODO: add CRL support +// https://github.com/notaryproject/notation-core-go/issues/125 func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { - if len(certChain) == 0 { - return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} - } - - // Validate cert chain structure - // Since this is using authentic signing time, signing time may be zero. - // Thus, it is better to pass nil here than fail for a cert's NotBefore - // being after zero time - switch r.certChainPurpose { - case ocsp.PurposeCodeSigning: - if err := coreX509.ValidateCodeSigningCertChain(certChain, nil); err != nil { - return nil, result.InvalidChainError{Err: err} - } - case ocsp.PurposeTimestamping: - if err := coreX509.ValidateTimestampingCertChain(certChain); err != nil { - return nil, result.InvalidChainError{Err: err} - } - default: - return nil, result.InvalidChainError{Err: fmt.Errorf("unknown certificate chain purpose %v", r.certChainPurpose)} - } - - ocspOpts := ocsp.Options{ + return ocsp.CheckStatus(ocsp.Options{ CertChain: certChain, - SigningTime: signingTime, CertChainPurpose: r.certChainPurpose, - HTTPClient: r.ocspHTTPClient, - } - - crlOpts := crl.Options{ - HTTPClient: r.crlHTTPClient, - SigningTime: signingTime, - } - - certResults := make([]*result.CertRevocationResult, len(certChain)) - var wg sync.WaitGroup - for i, cert := range certChain[:len(certChain)-1] { - switch { - case ocsp.SupportOCSP(cert): - // do OCSP check for the certificate - wg.Add(1) - - go func(i int, cert *x509.Certificate) { - defer wg.Done() - ocspResult := ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) - if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.SupportCRL(cert) { - // try CRL check if OCSP result is unknown - crlResult := crl.CertCheckStatus(r.ctx, cert, certChain[i+1], crlOpts) - crlResult.Error = result.OCSPFallbackError{ - OCSPErr: ocspResult.Error, - CRLErr: crlResult.Error, - } - certResults[i] = crlResult - } else { - certResults[i] = ocspResult - } - }(i, cert) - case crl.SupportCRL(cert): - // do CRL check for the certificate - wg.Add(1) - - go func(i int, cert *x509.Certificate) { - defer wg.Done() - certResults[i] = crl.CertCheckStatus(r.ctx, cert, certChain[i+1], crlOpts) - }(i, cert) - default: - certResults[i] = &result.CertRevocationResult{ - Result: result.ResultNonRevokable, - ServerResults: []*result.ServerResult{{ - Result: result.ResultNonRevokable, - Error: nil, - }}, - } - } - } + SigningTime: signingTime, + HTTPClient: r.httpClient, + }) - // Last is root cert, which will never be revoked by OCSP or CRL - certResults[len(certChain)-1] = &result.CertRevocationResult{ - Result: result.ResultNonRevokable, - ServerResults: []*result.ServerResult{{ - Result: result.ResultNonRevokable, - Error: nil, - }}, - } - wg.Wait() - return certResults, nil + // TODO: add CRL support + // https://github.com/notaryproject/notation-core-go/issues/125 } diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index 0ca1ec8a..f9d4f4e5 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -14,18 +14,10 @@ package revocation import ( - "bytes" - "context" - "crypto/rand" "crypto/x509" "errors" "fmt" - "io" - "math/big" "net/http" - "reflect" - "strconv" - "strings" "testing" "time" @@ -66,23 +58,6 @@ func validateEquivalentCertResults(certResults, expectedCertResults []*result.Ce t.Errorf("Expected certResults[%d].ServerResults[%d].Error to be %v, but got %v", i, j, expectedCertResults[i].ServerResults[j].Error, serverResult.Error) } } - - if len(certResult.CRLResults) != len(expectedCertResults[i].CRLResults) { - t.Errorf("Length of certResults[%d].CRLResults (%d) did not match expected length (%d)", i, len(certResult.CRLResults), len(expectedCertResults[i].CRLResults)) - } - - for j, crlResult := range certResult.CRLResults { - if crlResult.ReasonCode != expectedCertResults[i].CRLResults[j].ReasonCode { - t.Errorf("Expected certResults[%d].CRLResults[%d].ReasonCode to be %s, but got %s", i, j, expectedCertResults[i].CRLResults[j].ReasonCode, crlResult.ReasonCode) - } - - resultErrorType := reflect.TypeOf(crlResult.Error) - expectedErrorType := reflect.TypeOf(expectedCertResults[i].CRLResults[j].Error) - if resultErrorType != expectedErrorType { - t.Errorf("Expected certResults[%d].CRLResults[%d].Error to be of type %v, but got %v", i, j, expectedErrorType, resultErrorType) - } - - } } } @@ -95,13 +70,6 @@ func getOKCertResult(server string) *result.CertRevocationResult { } } -func getOKCertResultForCRL() *result.CertRevocationResult { - return &result.CertRevocationResult{ - Result: result.ResultOK, - CRLResults: []*result.CRLResult{}, - } -} - func getRootCertResult() *result.CertRevocationResult { return &result.CertRevocationResult{ Result: result.ResultNonRevokable, @@ -126,8 +94,8 @@ func TestNew(t *testing.T) { revR, ok := r.(*revocation) if !ok { t.Error("Expected New to create an object matching the internal revocation struct") - } else if revR.ocspHTTPClient != client { - t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.ocspHTTPClient) + } else if revR.httpClient != client { + t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.httpClient) } } @@ -258,7 +226,7 @@ func TestCheckRevocationStatusForRootCert(t *testing.T) { func TestCheckRevocationStatusForChain(t *testing.T) { zeroTime := time.Time{} - testChain := testhelper.GetRevokableRSAChain(6, true, false) + testChain := testhelper.GetRevokableRSAChain(6) revokableChain := make([]*x509.Certificate, 6) for i, tuple := range testChain { revokableChain[i] = tuple.Cert @@ -508,19 +476,6 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { revokableChain[i].NotBefore = zeroTime } - t.Run("invalid revocation purpose", func(t *testing.T) { - revocationClient := &revocation{ - ctx: context.Background(), - ocspHTTPClient: &http.Client{Timeout: 5 * time.Second}, - certChainPurpose: -1, - } - - _, err := revocationClient.Validate(revokableChain, time.Now()) - if err == nil { - t.Error("Expected Validate to fail with an error, but it succeeded") - } - }) - t.Run("empty chain", func(t *testing.T) { r, err := NewTimestamp(&http.Client{Timeout: 5 * time.Second}) if err != nil { @@ -535,21 +490,6 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { t.Error("Expected certResults to be nil when there is an error") } }) - - t.Run("invalid timestamping chain", func(t *testing.T) { - r, err := NewTimestamp(&http.Client{Timeout: 5 * time.Second}) - if err != nil { - t.Errorf("Expected successful creation of revocation, but received error: %v", err) - } - - certChain := testhelper.GetRevokableRSATimestampChain(3) - - _, err = r.Validate([]*x509.Certificate{certChain[0].Cert, certChain[1].Cert}, time.Now()) - if err == nil { - t.Errorf("Expected CheckStatus to fail, but got nil") - } - }) - t.Run("check non-revoked chain", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := NewTimestamp(client) @@ -765,7 +705,7 @@ func TestCheckRevocationErrors(t *testing.T) { rootCertTuple := testhelper.GetRSARootCertificate() noOCSPChain := []*x509.Certificate{leafCertTuple.Cert, rootCertTuple.Cert} - revokableTuples := testhelper.GetRevokableRSAChain(3, true, false) + revokableTuples := testhelper.GetRevokableRSAChain(3) noRootChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert} backwardsChain := []*x509.Certificate{revokableTuples[2].Cert, revokableTuples[1].Cert, revokableTuples[0].Cert} okChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert, revokableTuples[2].Cert} @@ -924,7 +864,7 @@ func TestCheckRevocationErrors(t *testing.T) { } func TestCheckRevocationInvalidChain(t *testing.T) { - revokableTuples := testhelper.GetRevokableRSAChain(4, true, false) + revokableTuples := testhelper.GetRevokableRSAChain(4) misorderedIntermediateTuples := []testhelper.RSACertTuple{revokableTuples[1], revokableTuples[0], revokableTuples[2], revokableTuples[3]} misorderedIntermediateChain := []*x509.Certificate{revokableTuples[1].Cert, revokableTuples[0].Cert, revokableTuples[2].Cert, revokableTuples[3].Cert} for i, cert := range misorderedIntermediateChain { @@ -977,235 +917,3 @@ func TestCheckRevocationInvalidChain(t *testing.T) { } }) } - -func TestCRL(t *testing.T) { - t.Run("CRL check valid", func(t *testing.T) { - chain := testhelper.GetRevokableRSAChain(3, false, true) - - revocationClient, err := NewWithOptions(context.Background(), &Options{ - CRLHTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: &crlRoundTripper{ - CertChain: chain, - Revoked: false, - }, - }, - OCSPHTTPClient: &http.Client{}, - }) - if err != nil { - t.Errorf("Expected successful creation of revocation, but received error: %v", err) - } - - certResults, err := revocationClient.Validate([]*x509.Certificate{ - chain[0].Cert, // leaf - chain[1].Cert, // intermediate - chain[2].Cert, // root - }, time.Now()) - if err != nil { - t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) - } - - expectedCertResults := []*result.CertRevocationResult{ - getOKCertResultForCRL(), - getOKCertResultForCRL(), - getRootCertResult(), - } - - validateEquivalentCertResults(certResults, expectedCertResults, t) - }) - - t.Run("CRL check with revoked status", func(t *testing.T) { - chain := testhelper.GetRevokableRSAChain(3, false, true) - - revocationClient, err := NewWithOptions(context.Background(), &Options{ - CRLHTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: &crlRoundTripper{ - CertChain: chain, - Revoked: true, - }, - }, - OCSPHTTPClient: &http.Client{}, - }) - if err != nil { - t.Errorf("Expected successful creation of revocation, but received error: %v", err) - } - - certResults, err := revocationClient.Validate([]*x509.Certificate{ - chain[0].Cert, // leaf - chain[1].Cert, // intermediate - chain[2].Cert, // root - }, time.Now()) - if err != nil { - t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) - } - - expectedCertResults := []*result.CertRevocationResult{ - { - Result: result.ResultRevoked, - CRLResults: []*result.CRLResult{ - { - ReasonCode: result.CRLReasonCodeKeyCompromise, - }, - }, - }, - { - Result: result.ResultRevoked, - CRLResults: []*result.CRLResult{ - { - ReasonCode: result.CRLReasonCodeKeyCompromise, - }, - }, - }, - getRootCertResult(), - } - - validateEquivalentCertResults(certResults, expectedCertResults, t) - }) - - t.Run("OCSP fallback to CRL", func(t *testing.T) { - chain := testhelper.GetRevokableRSAChain(3, true, true) - - revocationClient, err := NewWithOptions(context.Background(), &Options{ - CRLHTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: &crlRoundTripper{ - CertChain: chain, - Revoked: true, - FailOCSP: true, - }, - }, - OCSPHTTPClient: &http.Client{}, - }) - if err != nil { - t.Errorf("Expected successful creation of revocation, but received error: %v", err) - } - - certResults, err := revocationClient.Validate([]*x509.Certificate{ - chain[0].Cert, // leaf - chain[1].Cert, // intermediate - chain[2].Cert, // root - }, time.Now()) - if err != nil { - t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) - } - - expectedCertResults := []*result.CertRevocationResult{ - { - Result: result.ResultRevoked, - CRLResults: []*result.CRLResult{ - { - ReasonCode: result.CRLReasonCodeKeyCompromise, - }, - }, - Error: result.OCSPFallbackError{}, - }, - { - Result: result.ResultRevoked, - CRLResults: []*result.CRLResult{ - { - ReasonCode: result.CRLReasonCodeKeyCompromise, - }, - }, - Error: result.OCSPFallbackError{}, - }, - getRootCertResult(), - } - - validateEquivalentCertResults(certResults, expectedCertResults, t) - }) -} - -func TestNewWithOptions(t *testing.T) { - t.Run("nil ctx", func(t *testing.T) { - var ctx context.Context = nil - _, err := NewWithOptions(ctx, &Options{}) - if err == nil { - t.Error("Expected NewWithOptions to fail with an error, but it succeeded") - } - }) - - t.Run("nil OCSP HTTP Client", func(t *testing.T) { - _, err := NewWithOptions(context.Background(), &Options{}) - if err == nil { - t.Error("Expected NewWithOptions to fail with an error, but it succeeded") - } - }) - - t.Run("nil CRL HTTP Client", func(t *testing.T) { - _, err := NewWithOptions(context.Background(), &Options{ - OCSPHTTPClient: &http.Client{}, - }) - if err == nil { - t.Error("Expected NewWithOptions to fail with an error, but it succeeded") - } - }) - - t.Run("invalid CertChainPurpose", func(t *testing.T) { - _, err := NewWithOptions(context.Background(), &Options{ - OCSPHTTPClient: &http.Client{}, - CRLHTTPClient: &http.Client{}, - CertChainPurpose: -1, - }) - if err == nil { - t.Error("Expected NewWithOptions to fail with an error, but it succeeded") - } - }) - -} - -type crlRoundTripper struct { - CertChain []testhelper.RSACertTuple - Revoked bool - FailOCSP bool -} - -func (rt *crlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - // e.g. ocsp URL: http://example.com/chain_ocsp/0 - // e.g. crl URL: http://example.com/chain_crl/0 - parts := strings.Split(req.URL.Path, "/") - - isOCSP := parts[len(parts)-2] == "chain_ocsp" - // fail OCSP - if rt.FailOCSP && isOCSP { - return nil, errors.New("OCSP failed") - } - - // choose the cert suffix based on suffix of request url - // e.g. http://example.com/chain_crl/0 -> 0 - i, err := strconv.Atoi(parts[len(parts)-1]) - if err != nil { - return nil, err - } - if i >= len(rt.CertChain) { - return nil, errors.New("invalid index") - } - - cert := rt.CertChain[i].Cert - crl := &x509.RevocationList{ - NextUpdate: time.Now().Add(time.Hour), - Number: big.NewInt(20240720), - } - - if rt.Revoked { - crl.RevokedCertificateEntries = []x509.RevocationListEntry{ - { - SerialNumber: cert.SerialNumber, - RevocationTime: time.Now().Add(-time.Hour), - ReasonCode: int(result.CRLReasonCodeKeyCompromise), - }, - } - } - - issuerCert := rt.CertChain[i+1].Cert - issuerKey := rt.CertChain[i+1].PrivateKey - crlBytes, err := x509.CreateRevocationList(rand.Reader, crl, issuerCert, issuerKey) - if err != nil { - return nil, err - } - - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(crlBytes)), - }, nil -} From f50a9c3a3e95e412e7ea89376c66e1df354d1ef8 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 1 Aug 2024 08:56:26 +0000 Subject: [PATCH 052/110] fix: update Signed-off-by: Junjie Gao --- internal/encoding/asn1/asn1.go | 287 ----------------------- internal/encoding/asn1/asn1_test.go | 314 -------------------------- internal/encoding/asn1/common.go | 58 ----- internal/encoding/asn1/common_test.go | 86 ------- internal/encoding/asn1/constructed.go | 43 ---- internal/encoding/asn1/primitive.go | 41 ---- testhelper/certificatetest.go | 39 +--- 7 files changed, 12 insertions(+), 856 deletions(-) delete mode 100644 internal/encoding/asn1/asn1.go delete mode 100644 internal/encoding/asn1/asn1_test.go delete mode 100644 internal/encoding/asn1/common.go delete mode 100644 internal/encoding/asn1/common_test.go delete mode 100644 internal/encoding/asn1/constructed.go delete mode 100644 internal/encoding/asn1/primitive.go diff --git a/internal/encoding/asn1/asn1.go b/internal/encoding/asn1/asn1.go deleted file mode 100644 index 8e58294e..00000000 --- a/internal/encoding/asn1/asn1.go +++ /dev/null @@ -1,287 +0,0 @@ -// 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. -// - Indefinite length is not supported. -// - The length of the encoded data must fit the memory space of the int type (4 bytes). -// -// Reference: -// - http://luca.ntop.org/Teaching/Appunti/asn1.html -// - ISO/IEC 8825-1 -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. -// To get the DER of `r`, encode the values -// in the returned slice in order. -// -// Parameters: -// r - The input byte slice. -// -// Return: -// []value - The returned value, which is the flat slice of ASN.1 values, -// contains the nodes from a depth-first traversal. -// error - An error that can occur during the decoding process. -// -// Reference: ISO/IEC 8825-1: 8.1.1.3 -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. -// -// Parameters: -// r - The input byte slice. -// -// Return: -// []byte - The identifier octets. -// []byte - The content octets. -// []byte - The subsequent octets after the value. -// error - An error that can occur during the decoding process. -// -// Reference: ISO/IEC 8825-1: 8.1.1.3 -func decodeMetadata(r []byte) ([]byte, []byte, []byte, error) { - // structure of an encoding (primitive or constructed) - // +----------------+----------------+----------------+ - // | identifier | length | content | - // +----------------+----------------+----------------+ - 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. -// -// Parameters: -// r - The input byte slice from which the identifier octets are to be decoded. -// -// Returns: -// []byte - The identifier octets decoded from the input byte slice. -// []byte - The remaining part of the input byte slice after the identifier octets. -// error - An error that can occur during the decoding process. -// -// Reference: ISO/IEC 8825-1: 8.1.2 -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 - // Reference: ISO/IEC 8825-1: 8.1.2.4 - 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 -// -// Parameters: -// r - The input byte slice from which the length octets are to be decoded. -// -// Returns: -// int - The length decoded from the input byte slice. -// []byte - The remaining part of the input byte slice after the length octets. -// error - An error that can occur during the decoding process. -// -// Reference: ISO/IEC 8825-1: 8.1.3 -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 - // Reference: ISO/IEC 8825-1: 8.1.3.4 - return int(b), r[offset:], nil - } else if b == 0x80 { - // Indefinite-length method is not supported. - // Reference: ISO/IEC 8825-1: 8.1.3.6.1 - return 0, nil, ErrUnsupportedIndefiniteLength - } - - // long form - // Reference: ISO/IEC 8825-1: 8.1.3.5 - n := int(b & 0x7f) - if n > 4 { - // length must fit the memory space of the int type (4 bytes). - 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. -// Reference: ISO/IEC 8825-1: 8.1.2.5 -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 deleted file mode 100644 index 3cc94795..00000000 --- a/internal/encoding/asn1/asn1_test.go +++ /dev/null @@ -1,314 +0,0 @@ -// 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 ( - "fmt" - "reflect" - "testing" -) - -func TestConvertToDER(t *testing.T) { - testData := []struct { - name string - ber []byte - der []byte - expectError bool - }{ - { - name: "Constructed value", - 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: []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2d, - - // Type identifier - 0x06, - // Type length - 0x09, - // Type content - 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, - - // Value identifier - 0x04, - // Value length in BER - 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, - }, - expectError: false, - }, - { - name: "Primitive value", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0x20, - // length - 0x81, 0x01, - // content - 0x01, - }, - der: []byte{ - // Primitive value - // identifier - 0x1f, 0x20, - // length - 0x01, - // content - 0x01, - }, - expectError: false, - }, - { - name: "Constructed value in constructed value", - ber: []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2d, - - // Constructed value identifier - 0x26, - // Type length - 0x2b, - - // Value identifier - 0x04, - // Value length in BER - 0x81, 0x28, - // Value content - 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, - 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: []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2c, - - // Constructed value identifier - 0x26, - // Type length - 0x2a, - - // Value identifier - 0x04, - // Value length in BER - 0x28, - // Value content - 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, - 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, - }, - expectError: false, - }, - { - name: "empty", - ber: []byte{}, - der: []byte{}, - expectError: true, - }, - { - name: "identifier high tag number form", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x81, 0x01, - // content - 0x01, - }, - der: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x01, - // content - 0x01, - }, - expectError: false, - }, - { - name: "EOF for identifier high tag number form", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, - }, - der: []byte{}, - expectError: true, - }, - { - name: "EOF for length", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - }, - der: []byte{}, - expectError: true, - }, - { - name: "Unsupport indefinite-length", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x80, - }, - der: []byte{}, - expectError: true, - }, - { - name: "length greater than 4 bytes", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x85, - }, - der: []byte{}, - expectError: true, - }, - { - name: "long form length EOF ", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x84, - }, - der: []byte{}, - expectError: true, - }, - { - name: "length greater > int32", - ber: append([]byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x84, 0xFF, 0xFF, 0xFF, 0xFF, - }, make([]byte, 0xFFFFFFFF)...), - der: []byte{}, - expectError: true, - }, - { - name: "length greater than content", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x02, - }, - der: []byte{}, - expectError: true, - }, - { - name: "trailing data", - ber: []byte{ - // Primitive value - // identifier - 0x1f, 0xa0, 0x20, - // length - 0x02, - // content - 0x01, 0x02, 0x03, - }, - der: []byte{}, - expectError: true, - }, - { - name: "EOF in constructed value", - ber: []byte{ - // Constructed value - 0x30, - // Constructed value length - 0x2c, - - // Constructed value identifier - 0x26, - // Type length - 0x2b, - - // Value identifier - 0x04, - // Value length in BER - 0x81, 0x28, - // Value content - 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, - 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, - }, - expectError: true, - }, - } - - for _, tt := range testData { - der, err := ConvertToDER(tt.ber) - fmt.Printf("DER: %x\n", der) - if !tt.expectError && err != nil { - t.Errorf("ConvertToDER() error = %v, but expect no error", err) - return - } - if tt.expectError && err == nil { - t.Errorf("ConvertToDER() error = nil, but expect error") - } - - if !tt.expectError && !reflect.DeepEqual(der, tt.der) { - t.Errorf("got = %v, want %v", der, tt.der) - } - } -} diff --git a/internal/encoding/asn1/common.go b/internal/encoding/asn1/common.go deleted file mode 100644 index 7c7110e7..00000000 --- a/internal/encoding/asn1/common.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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. -// Reference: ISO/IEC 8825-1: 10.1 -func encodeLength(w io.ByteWriter, length int) error { - if length < 0 { - return ErrUnsupportedLength - } - - // 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. -// Reference: ISO/IEC 8825-1: 10.1 -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/common_test.go b/internal/encoding/asn1/common_test.go deleted file mode 100644 index 1dd781ca..00000000 --- a/internal/encoding/asn1/common_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package asn1 - -import ( - "bytes" - "testing" -) - -func TestEncodeLength(t *testing.T) { - tests := []struct { - name string - length int - want []byte - wantErr bool - }{ - { - name: "Length less than 128", - length: 127, - want: []byte{127}, - wantErr: false, - }, - { - name: "Length equal to 128", - length: 128, - want: []byte{0x81, 128}, - wantErr: false, - }, - { - name: "Length greater than 128", - length: 300, - want: []byte{0x82, 0x01, 0x2C}, - wantErr: false, - }, - { - name: "Negative length", - length: -1, - want: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf := &bytes.Buffer{} - err := encodeLength(buf, tt.length) - if (err != nil) != tt.wantErr { - t.Errorf("encodeLength() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got := buf.Bytes(); !bytes.Equal(got, tt.want) { - t.Errorf("encodeLength() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestEncodedLengthSize(t *testing.T) { - tests := []struct { - name string - length int - want int - }{ - { - name: "Length less than 128", - length: 127, - want: 1, - }, - { - name: "Length equal to 128", - length: 128, - want: 2, - }, - { - name: "Length greater than 128", - length: 300, - want: 3, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := encodedLengthSize(tt.length); got != tt.want { - t.Errorf("encodedLengthSize() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/encoding/asn1/constructed.go b/internal/encoding/asn1/constructed.go deleted file mode 100644 index 409fd5a4..00000000 --- a/internal/encoding/asn1/constructed.go +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index 0c2cdec6..00000000 --- a/internal/encoding/asn1/primitive.go +++ /dev/null @@ -1,41 +0,0 @@ -// 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 -} diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index ffb87653..54a31c0e 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -76,15 +76,15 @@ func GetRevokableRSALeafCertificate() RSACertTuple { } // GetRevokableRSAChain returns a chain of certificates that specify a local OCSP server signed using RSA algorithm -func GetRevokableRSAChain(size int, enabledOCSP, enabledCRL bool) []RSACertTuple { +func GetRevokableRSAChain(size int) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) - chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, enabledCRL) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) for i := size - 2; i > 0; i-- { - chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, enabledOCSP, enabledCRL) + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) } if size > 1 { - chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false, enabledOCSP, enabledCRL) + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false) } return chain } @@ -94,12 +94,12 @@ func GetRevokableRSAChain(size int, enabledOCSP, enabledCRL bool) []RSACertTuple func GetRevokableRSATimestampChain(size int) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) - chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, false) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) for i := size - 2; i > 0; i-- { - chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, true, false) + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) } if size > 1 { - chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true, true, false) + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true) } return chain } @@ -171,46 +171,31 @@ func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } -func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int, enabledOCSP, enabledCRL bool) RSACertTuple { +func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) RSACertTuple { template := getCertTemplate(previous == nil, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign - if enabledOCSP { - template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} - } - if enabledCRL { - template.KeyUsage |= x509.KeyUsageCRLSign - template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d", index)} - - } + template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} return getRSACertTupleWithTemplate(template, previous.PrivateKey, previous) } -func getRevokableRSARootChainCertTuple(cn string, pathLen int, enabledCRL bool) RSACertTuple { +func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) template := getCertTemplate(true, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign - if enabledCRL { - template.KeyUsage |= x509.KeyUsageCRLSign - } template.MaxPathLen = pathLen return getRSACertTupleWithTemplate(template, pk, nil) } -func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp, enabledOCSP, enabledCRL bool) RSACertTuple { +func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp bool) RSACertTuple { template := getCertTemplate(false, codesign, timestamp, cn) template.BasicConstraintsValid = true template.IsCA = false template.KeyUsage = x509.KeyUsageDigitalSignature - if enabledOCSP { - template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} - } - if enabledCRL { - template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d", index)} - } + template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } From cb771bb2ed6e914b30cb3da7641ae7fc80849c41 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 1 Aug 2024 09:09:08 +0000 Subject: [PATCH 053/110] fix: update Signed-off-by: Junjie Gao --- revocation/internal/crl/cache/crl.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/revocation/internal/crl/cache/crl.go b/revocation/internal/crl/cache/crl.go index 8420a95d..c6e8c731 100644 --- a/revocation/internal/crl/cache/crl.go +++ b/revocation/internal/crl/cache/crl.go @@ -20,6 +20,8 @@ const ( // CRL is in memory representation of the CRL tarball, including CRL file and // metadata file, which may be cached in the file system or other storage +// +// TODO: consider adding DeltaCRL field in the future type CRL struct { BaseCRL *x509.RevocationList Metadata Metadata From f79d4a38c05e1df5c5ebc18a81d4d29e845fd3af Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 2 Aug 2024 02:21:01 +0000 Subject: [PATCH 054/110] fix: update Signed-off-by: Junjie Gao --- .../crl/cache/crl.go => crl/cache/bundle.go} | 33 ++++++++++--------- revocation/{internal => }/crl/cache/cache.go | 8 ++--- revocation/{internal => }/crl/cache/dummy.go | 4 +-- revocation/{internal => }/crl/cache/error.go | 0 revocation/{internal => }/crl/cache/fs.go | 17 +++------- revocation/internal/crl/fetch.go | 17 ++++------ 6 files changed, 32 insertions(+), 47 deletions(-) rename revocation/{internal/crl/cache/crl.go => crl/cache/bundle.go} (77%) rename revocation/{internal => }/crl/cache/cache.go (81%) rename revocation/{internal => }/crl/cache/dummy.go (73%) rename revocation/{internal => }/crl/cache/error.go (100%) rename revocation/{internal => }/crl/cache/fs.go (81%) diff --git a/revocation/internal/crl/cache/crl.go b/revocation/crl/cache/bundle.go similarity index 77% rename from revocation/internal/crl/cache/crl.go rename to revocation/crl/cache/bundle.go index c6e8c731..a8361a89 100644 --- a/revocation/internal/crl/cache/crl.go +++ b/revocation/crl/cache/bundle.go @@ -18,16 +18,19 @@ const ( MetadataFile = "metadata.json" ) -// CRL is in memory representation of the CRL tarball, including CRL file and -// metadata file, which may be cached in the file system or other storage +// Bundle is in memory representation of the Bundle tarball, including base CRL +// file and metadata file, which may be cached in the file system or other +// storage // // TODO: consider adding DeltaCRL field in the future -type CRL struct { +type Bundle struct { BaseCRL *x509.RevocationList Metadata Metadata } // Metadata stores the metadata infomation of the CRL +// +// TODO: consider adding DeltaCRL field in the future type Metadata struct { BaseCRL FileInfo `json:"base.crl"` } @@ -38,9 +41,9 @@ type FileInfo struct { CreateAt time.Time `json:"createAt"` } -// NewCRL creates a new CRL store with tarball format -func NewCRL(baseCRL *x509.RevocationList, url string) (*CRL, error) { - crl := &CRL{ +// NewBundle creates a new CRL store with tarball format +func NewBundle(baseCRL *x509.RevocationList, url string) (*Bundle, error) { + crl := &Bundle{ BaseCRL: baseCRL, Metadata: Metadata{ BaseCRL: FileInfo{ @@ -53,7 +56,7 @@ func NewCRL(baseCRL *x509.RevocationList, url string) (*CRL, error) { return crl, nil } -// ParseCRLFromTarball parses the CRL blob from a tarball +// ParseBundleFromTarball parses the CRL blob from a tarball // // The tarball should contain two files: // - base.crl: the base CRL in DER format @@ -67,8 +70,8 @@ func NewCRL(baseCRL *x509.RevocationList, url string) (*CRL, error) { // "createAt": "2021-09-01T00:00:00Z" // } // } -func ParseCRLFromTarball(data io.Reader) (*CRL, error) { - crl := &CRL{} +func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { + crl := &Bundle{} // parse the tarball tar := tar.NewReader(data) @@ -153,27 +156,27 @@ func ParseCRLFromTarball(data io.Reader) (*CRL, error) { // "createAt": "2021-09-01T00:00:00Z" // } // } -func SaveAsTarball(w io.Writer, crl *CRL) (err error) { +func SaveAsTarball(w io.Writer, bundle *Bundle) (err error) { tarWriter := tar.NewWriter(w) // Add base.crl - if err := addToTar(BaseCRLFile, crl.BaseCRL.Raw, tarWriter); err != nil { + if err := addToTar(BaseCRLFile, bundle.BaseCRL.Raw, bundle.Metadata.BaseCRL.CreateAt, tarWriter); err != nil { return err } // Add metadata.json - metadataBytes, err := json.Marshal(crl.Metadata) + metadataBytes, err := json.Marshal(bundle.Metadata) if err != nil { return err } - return addToTar(MetadataFile, metadataBytes, tarWriter) + return addToTar(MetadataFile, metadataBytes, time.Now(), tarWriter) } -func addToTar(fileName string, data []byte, tw *tar.Writer) error { +func addToTar(fileName string, data []byte, modTime time.Time, tw *tar.Writer) error { header := &tar.Header{ Name: fileName, Size: int64(len(data)), Mode: 0644, - ModTime: time.Now(), + ModTime: modTime, } if err := tw.WriteHeader(header); err != nil { return err diff --git a/revocation/internal/crl/cache/cache.go b/revocation/crl/cache/cache.go similarity index 81% rename from revocation/internal/crl/cache/cache.go rename to revocation/crl/cache/cache.go index 29bfa63c..0495a090 100644 --- a/revocation/internal/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -24,14 +24,10 @@ type Cache interface { // Get retrieves the content with the given key // // - if the key does not exist, return os.ErrNotExist - // - when request a key of a CRL, the implementation MUST return a *CRL - Get(ctx context.Context, key string) (any, error) + Get(ctx context.Context, key string) (*Bundle, error) // Set stores the content with the given key - // - // the implementation MUST support *CRL as the type of value field to - // cache a CRL - Set(ctx context.Context, key string, value any) error + Set(ctx context.Context, key string, bundle *Bundle) error // Delete removes the content with the given key Delete(ctx context.Context, key string) error diff --git a/revocation/internal/crl/cache/dummy.go b/revocation/crl/cache/dummy.go similarity index 73% rename from revocation/internal/crl/cache/dummy.go rename to revocation/crl/cache/dummy.go index 10702d33..7d5b3a70 100644 --- a/revocation/internal/crl/cache/dummy.go +++ b/revocation/crl/cache/dummy.go @@ -14,11 +14,11 @@ func NewDummyCache() Cache { return &dummyCache{} } -func (c *dummyCache) Get(ctx context.Context, key string) (any, error) { +func (c *dummyCache) Get(ctx context.Context, key string) (*Bundle, error) { return nil, os.ErrNotExist } -func (c *dummyCache) Set(ctx context.Context, key string, value any) error { +func (c *dummyCache) Set(ctx context.Context, key string, bundle *Bundle) error { return nil } diff --git a/revocation/internal/crl/cache/error.go b/revocation/crl/cache/error.go similarity index 100% rename from revocation/internal/crl/cache/error.go rename to revocation/crl/cache/error.go diff --git a/revocation/internal/crl/cache/fs.go b/revocation/crl/cache/fs.go similarity index 81% rename from revocation/internal/crl/cache/fs.go rename to revocation/crl/cache/fs.go index 54a7f107..5657beb3 100644 --- a/revocation/internal/crl/cache/fs.go +++ b/revocation/crl/cache/fs.go @@ -2,7 +2,6 @@ package cache import ( "context" - "fmt" "os" "path/filepath" "time" @@ -39,14 +38,14 @@ func NewFileSystemCache(dir string, ttl time.Duration) (Cache, error) { }, nil } -func (c *fileSystemCache) Get(ctx context.Context, key string) (any, error) { +func (c *fileSystemCache) Get(ctx context.Context, key string) (*Bundle, error) { f, err := os.Open(filepath.Join(c.dir, key)) if err != nil { return nil, err } defer f.Close() - blob, err := ParseCRLFromTarball(f) + blob, err := ParseBundleFromTarball(f) if err != nil { return nil, err } @@ -58,20 +57,12 @@ func (c *fileSystemCache) Get(ctx context.Context, key string) (any, error) { return blob, nil } -func (c *fileSystemCache) Set(ctx context.Context, key string, value any) error { - var crlBlob *CRL - switch v := value.(type) { - case *CRL: - crlBlob = v - default: - return fmt.Errorf("invalid value type: %T", value) - } - +func (c *fileSystemCache) Set(ctx context.Context, key string, bundle *Bundle) error { tempFile, err := os.CreateTemp("", tempFileName) if err != nil { return err } - if err := SaveAsTarball(tempFile, crlBlob); err != nil { + if err := SaveAsTarball(tempFile, bundle); err != nil { return err } diff --git a/revocation/internal/crl/fetch.go b/revocation/internal/crl/fetch.go index 39fc9e68..7a6c1a6a 100644 --- a/revocation/internal/crl/fetch.go +++ b/revocation/internal/crl/fetch.go @@ -13,36 +13,31 @@ import ( "os" "strings" - "github.com/notaryproject/notation-core-go/revocation/internal/crl/cache" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) // maxCRLSize is the maximum size of CRL in bytes const maxCRLSize = 10 << 20 // 10 MiB -func fetch(ctx context.Context, cacheClient cache.Cache, crlURL string, client *http.Client) (*cache.CRL, error) { +func fetch(ctx context.Context, cacheClient cache.Cache, crlURL string, client *http.Client) (*cache.Bundle, error) { // check cache // try to get from cache - obj, err := cacheClient.Get(ctx, tarStoreName(crlURL)) + crlBundle, err := cacheClient.Get(ctx, tarStoreName(crlURL)) if err != nil { - var cacheBrokenError cache.BrokenFileError + var cacheBrokenError *cache.BrokenFileError if os.IsNotExist(err) || errors.As(err, &cacheBrokenError) { crl, err := download(ctx, crlURL, client) if err != nil { return nil, err } - return cache.NewCRL(crl, crlURL) + return cache.NewBundle(crl, crlURL) } return nil, err } - crl, ok := obj.(*cache.CRL) - if !ok { - return nil, fmt.Errorf("invalid cache object type: %T", obj) - } - - return crl, nil + return crlBundle, nil } func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { From 5bee5b681565e80f0e8a6c57d293508d21af646a Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 2 Aug 2024 02:23:20 +0000 Subject: [PATCH 055/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 12 ++++++------ revocation/crl/cache/fs.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index a8361a89..463629f4 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -12,10 +12,10 @@ import ( const ( // BaseCRL is the file name of the base CRL - BaseCRLFile = "base.crl" + PathBaseCRL = "base.crl" // Metadata is the file name of the metadata - MetadataFile = "metadata.json" + PathMetadata = "metadata.json" ) // Bundle is in memory representation of the Bundle tarball, including base CRL @@ -87,7 +87,7 @@ func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { } switch header.Name { - case BaseCRLFile: + case PathBaseCRL: // parse base.crl data, err := io.ReadAll(tar) if err != nil { @@ -103,7 +103,7 @@ func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { } crl.BaseCRL = baseCRL - case MetadataFile: + case PathMetadata: // parse metadata var metadata Metadata if err := json.NewDecoder(tar).Decode(&metadata); err != nil { @@ -159,7 +159,7 @@ func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { func SaveAsTarball(w io.Writer, bundle *Bundle) (err error) { tarWriter := tar.NewWriter(w) // Add base.crl - if err := addToTar(BaseCRLFile, bundle.BaseCRL.Raw, bundle.Metadata.BaseCRL.CreateAt, tarWriter); err != nil { + if err := addToTar(PathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.BaseCRL.CreateAt, tarWriter); err != nil { return err } @@ -168,7 +168,7 @@ func SaveAsTarball(w io.Writer, bundle *Bundle) (err error) { if err != nil { return err } - return addToTar(MetadataFile, metadataBytes, time.Now(), tarWriter) + return addToTar(PathMetadata, metadataBytes, time.Now(), tarWriter) } func addToTar(fileName string, data []byte, modTime time.Time, tw *tar.Writer) error { diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go index 5657beb3..9077d519 100644 --- a/revocation/crl/cache/fs.go +++ b/revocation/crl/cache/fs.go @@ -24,7 +24,7 @@ type fileSystemCache struct { // NewFileSystemCache creates a new file system store func NewFileSystemCache(dir string, ttl time.Duration) (Cache, error) { - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0700); err != nil { return nil, err } From 383e22edc5cdd10b441944add1eb4a2f77d980bd Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 2 Aug 2024 08:36:32 +0000 Subject: [PATCH 056/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/cache.go | 6 +++++- revocation/crl/cache/dummy.go | 31 ------------------------------- revocation/crl/cache/fs.go | 31 ++++++++++--------------------- revocation/internal/crl/fetch.go | 32 +++++++++++++++++--------------- 4 files changed, 32 insertions(+), 68 deletions(-) delete mode 100644 revocation/crl/cache/dummy.go diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index 0495a090..086a4053 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -17,14 +17,18 @@ package cache import ( "context" + "time" ) // Cache is an interface that specifies methods used for caching type Cache interface { // Get retrieves the content with the given key // + // maxAge is the maximum age of the content. If the content is older than + // maxAge, it will be considered as expired and should not be returned. + // // - if the key does not exist, return os.ErrNotExist - Get(ctx context.Context, key string) (*Bundle, error) + Get(ctx context.Context, key string, maxAge time.Duration) (*Bundle, error) // Set stores the content with the given key Set(ctx context.Context, key string, bundle *Bundle) error diff --git a/revocation/crl/cache/dummy.go b/revocation/crl/cache/dummy.go deleted file mode 100644 index 7d5b3a70..00000000 --- a/revocation/crl/cache/dummy.go +++ /dev/null @@ -1,31 +0,0 @@ -package cache - -import ( - "context" - "os" -) - -// dummyCache is a dummy cache implementation that does nothing -type dummyCache struct { -} - -// NewDummyCache creates a new dummy cache -func NewDummyCache() Cache { - return &dummyCache{} -} - -func (c *dummyCache) Get(ctx context.Context, key string) (*Bundle, error) { - return nil, os.ErrNotExist -} - -func (c *dummyCache) Set(ctx context.Context, key string, bundle *Bundle) error { - return nil -} - -func (c *dummyCache) Delete(ctx context.Context, key string) error { - return nil -} - -func (c *dummyCache) Clear(ctx context.Context) error { - return nil -} diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go index 9077d519..d3ea7e90 100644 --- a/revocation/crl/cache/fs.go +++ b/revocation/crl/cache/fs.go @@ -8,37 +8,26 @@ import ( ) const ( - // DefaultTTL is the default time to live for the cache - DefaultTTL = 24 * 7 * time.Hour - // tempFileName is the prefix of the temporary file tempFileName = "notation-*" ) -// fileSystemCache builds on top of OS file system to leverage the file system +// fileCache builds on top of OS file system to leverage the file system // concurrency control and atomicity -type fileSystemCache struct { +type fileCache struct { dir string - ttl time.Duration } -// NewFileSystemCache creates a new file system store -func NewFileSystemCache(dir string, ttl time.Duration) (Cache, error) { +// NewFileCache creates a new file system store +func NewFileCache(dir string, ttl time.Duration) (Cache, error) { if err := os.MkdirAll(dir, 0700); err != nil { return nil, err } - if ttl == 0 { - ttl = DefaultTTL - } - - return &fileSystemCache{ - dir: dir, - ttl: ttl, - }, nil + return &fileCache{dir: dir}, nil } -func (c *fileSystemCache) Get(ctx context.Context, key string) (*Bundle, error) { +func (c *fileCache) Get(ctx context.Context, key string, maxAge time.Duration) (*Bundle, error) { f, err := os.Open(filepath.Join(c.dir, key)) if err != nil { return nil, err @@ -50,14 +39,14 @@ func (c *fileSystemCache) Get(ctx context.Context, key string) (*Bundle, error) return nil, err } - if time.Since(blob.Metadata.BaseCRL.CreateAt) > c.ttl { + if maxAge != 0 && time.Since(blob.Metadata.BaseCRL.CreateAt) > maxAge { return nil, os.ErrNotExist } return blob, nil } -func (c *fileSystemCache) Set(ctx context.Context, key string, bundle *Bundle) error { +func (c *fileCache) Set(ctx context.Context, key string, bundle *Bundle) error { tempFile, err := os.CreateTemp("", tempFileName) if err != nil { return err @@ -71,10 +60,10 @@ func (c *fileSystemCache) Set(ctx context.Context, key string, bundle *Bundle) e return os.Rename(tempFile.Name(), filepath.Join(c.dir, key)) } -func (c *fileSystemCache) Delete(ctx context.Context, key string) error { +func (c *fileCache) Delete(ctx context.Context, key string) error { return os.Remove(filepath.Join(c.dir, key)) } -func (c *fileSystemCache) Clear(ctx context.Context) error { +func (c *fileCache) Clear(ctx context.Context) error { return os.RemoveAll(c.dir) } diff --git a/revocation/internal/crl/fetch.go b/revocation/internal/crl/fetch.go index 7a6c1a6a..35c64f06 100644 --- a/revocation/internal/crl/fetch.go +++ b/revocation/internal/crl/fetch.go @@ -11,42 +11,40 @@ import ( "net/http" "net/url" "os" - "strings" + "time" "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) // maxCRLSize is the maximum size of CRL in bytes -const maxCRLSize = 10 << 20 // 10 MiB +const maxCRLSize = 64 * 1024 * 1024 // 64 MiB + +func fetch(ctx context.Context, c cache.Cache, crlURL string, cacheMaxAge time.Duration, httpClient *http.Client) (*cache.Bundle, error) { + if c == nil { + return download(ctx, crlURL, httpClient) + } -func fetch(ctx context.Context, cacheClient cache.Cache, crlURL string, client *http.Client) (*cache.Bundle, error) { - // check cache // try to get from cache - crlBundle, err := cacheClient.Get(ctx, tarStoreName(crlURL)) + crlBundle, err := c.Get(ctx, tarStoreName(crlURL), cacheMaxAge) if err != nil { var cacheBrokenError *cache.BrokenFileError if os.IsNotExist(err) || errors.As(err, &cacheBrokenError) { - crl, err := download(ctx, crlURL, client) - if err != nil { - return nil, err - } - - return cache.NewBundle(crl, crlURL) + // download if not exist or broken + return download(ctx, crlURL, httpClient) } - return nil, err } return crlBundle, nil } -func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { +func download(ctx context.Context, crlURL string, client *http.Client) (*cache.Bundle, error) { // validate URL parsedURL, err := url.Parse(crlURL) if err != nil { return nil, fmt.Errorf("invalid CRL URL: %w", err) } - if strings.ToLower(parsedURL.Scheme) != "http" { + if parsedURL.Scheme != "http" { return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme) } @@ -76,7 +74,11 @@ func download(ctx context.Context, crlURL string, client *http.Client) (*x509.Re return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize) } - return x509.ParseRevocationList(data) + crl, err := x509.ParseRevocationList(data) + if err != nil { + return nil, fmt.Errorf("failed to parse CRL: %w", err) + } + return cache.NewBundle(crl, crlURL) } func tarStoreName(url string) string { From 1ae6d1ccf3587932be56d9b0dc0e2b900a15e016 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 8 Aug 2024 02:05:15 +0000 Subject: [PATCH 057/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 20 ++-- revocation/crl/cache/cache.go | 19 ++-- revocation/crl/cache/file.go | 116 +++++++++++++++++++++++ revocation/crl/cache/fs.go | 69 -------------- revocation/crl/cache/memory.go | 76 +++++++++++++++ revocation/internal/crl/fetch.go | 92 ------------------- revocation/internal/crl/fetcher.go | 143 +++++++++++++++++++++++++++++ 7 files changed, 356 insertions(+), 179 deletions(-) create mode 100644 revocation/crl/cache/file.go delete mode 100644 revocation/crl/cache/fs.go create mode 100644 revocation/crl/cache/memory.go delete mode 100644 revocation/internal/crl/fetch.go create mode 100644 revocation/internal/crl/fetcher.go diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index 463629f4..b779386d 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -32,28 +32,26 @@ type Bundle struct { // // TODO: consider adding DeltaCRL field in the future type Metadata struct { - BaseCRL FileInfo `json:"base.crl"` + BaseCRL FileInfo `json:"base.crl"` + CreateAt time.Time `json:"createAt"` } // FileInfo stores the URL and creation time of the file type FileInfo struct { - URL string `json:"url"` - CreateAt time.Time `json:"createAt"` + URL string `json:"url"` } // NewBundle creates a new CRL store with tarball format func NewBundle(baseCRL *x509.RevocationList, url string) (*Bundle, error) { - crl := &Bundle{ + return &Bundle{ BaseCRL: baseCRL, Metadata: Metadata{ BaseCRL: FileInfo{ - URL: url, - CreateAt: time.Now(), + URL: url, }, + CreateAt: time.Now(), }, - } - - return crl, nil + }, nil } // ParseBundleFromTarball parses the CRL blob from a tarball @@ -132,7 +130,7 @@ func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { Err: errors.New("base CRL URL is missing from cached tarball"), } } - if crl.Metadata.BaseCRL.CreateAt.IsZero() { + if crl.Metadata.CreateAt.IsZero() { return nil, &BrokenFileError{ Err: errors.New("base CRL creation time is missing from cached tarball"), } @@ -159,7 +157,7 @@ func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { func SaveAsTarball(w io.Writer, bundle *Bundle) (err error) { tarWriter := tar.NewWriter(w) // Add base.crl - if err := addToTar(PathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.BaseCRL.CreateAt, tarWriter); err != nil { + if err := addToTar(PathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CreateAt, tarWriter); err != nil { return err } diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index 086a4053..46669b10 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -20,22 +20,27 @@ import ( "time" ) +const ( + // DefaultMaxAge is the default maximum age of the CRLs cache. + // If the CRL is older than DefaultMaxAge, it will be considered as expired. + DefaultMaxAge = 24 * 7 * time.Hour +) + // Cache is an interface that specifies methods used for caching type Cache interface { // Get retrieves the content with the given key // - // maxAge is the maximum age of the content. If the content is older than - // maxAge, it will be considered as expired and should not be returned. - // // - if the key does not exist, return os.ErrNotExist - Get(ctx context.Context, key string, maxAge time.Duration) (*Bundle, error) + Get(ctx context.Context, key string) (*Bundle, error) // Set stores the content with the given key - Set(ctx context.Context, key string, bundle *Bundle) error + // + // - expiration is the time duration before the content is valid + Set(ctx context.Context, key string, value *Bundle) error // Delete removes the content with the given key Delete(ctx context.Context, key string) error - // Clear removes all content - Clear(ctx context.Context) error + // Flush removes all content + Flush(ctx context.Context) error } diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go new file mode 100644 index 00000000..dd516c6c --- /dev/null +++ b/revocation/crl/cache/file.go @@ -0,0 +1,116 @@ +package cache + +import ( + "context" + "os" + "path/filepath" + "time" +) + +const ( + // tempFileName is the prefix of the temporary file + tempFileName = "notation-*" + + deletedExt = ".deleted" + defaultRetryDelay = 100 * time.Millisecond + defaultRetryAttempts = 5 +) + +// FileCache stores in a tarball format, which contains two files: base.crl and +// metadata.json. The base.crl file contains the base CRL in DER format, and the +// metadata.json file contains the metadata of the CRL. The cache builds on top +// of UNIX file system to leverage the file system concurrency control and +// atomicity. +// +// NOTE: For Windows, the atomicity is not guaranteed. Please avoid using this +// cache on Windows when the concurrent write is required. +// +// FileCache doesn't handle cache cleaning but provides the Delete and Clear +// methods to remove the CRLs from the file system. +type FileCache struct { + dir string + maxAge time.Duration +} + +type FileCacheOptions struct { + Dir string + MaxAge time.Duration +} + +// NewFileCache creates a new file system store +// +// - dir is the directory to store the CRLs. +// - maxAge is the maximum age of the CRLs cache. If the CRL is older than +// maxAge, it will be considered as expired. +func NewFileCache(opts *FileCacheOptions) (*FileCache, error) { + if err := os.MkdirAll(opts.Dir, 0700); err != nil { + return nil, err + } + + cache := &FileCache{ + dir: opts.Dir, + maxAge: opts.MaxAge, + } + if cache.maxAge == 0 { + cache.maxAge = DefaultMaxAge + } + return cache, nil +} + +// Get retrieves the CRL bundle from the file system +// +// - if the key does not exist, return os.ErrNotExist +// - if the CRL is expired, return os.ErrNotExist +func (c *FileCache) Get(ctx context.Context, key string) (*Bundle, error) { + f, err := os.Open(filepath.Join(c.dir, key)) + if err != nil { + return nil, err + } + defer f.Close() + + bundle, err := ParseBundleFromTarball(f) + if err != nil { + return nil, err + } + + if c.maxAge > 0 && time.Now().After(bundle.Metadata.CreateAt.Add(c.maxAge)) { + return nil, os.ErrNotExist + } + + return bundle, nil +} + +// Set stores the CRL bundle in the file system +func (c *FileCache) Set(ctx context.Context, key string, bundle *Bundle, expiration time.Duration) error { + // save to temp file + tempFile, err := os.CreateTemp("", tempFileName) + if err != nil { + return err + } + if err := SaveAsTarball(tempFile, bundle); err != nil { + return err + } + tempFile.Close() + + // rename is atomic on UNIX platforms + return os.Rename(tempFile.Name(), filepath.Join(c.dir, key)) +} + +// Delete removes the CRL bundle file from file system +func (c *FileCache) Delete(ctx context.Context, key string) error { + return os.Remove(filepath.Join(c.dir, key)) +} + +// Clear removes all CRLs from the file system +func (c *FileCache) Clear(ctx context.Context) error { + return filepath.Walk(c.dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + return os.Remove(path) + }) +} diff --git a/revocation/crl/cache/fs.go b/revocation/crl/cache/fs.go deleted file mode 100644 index d3ea7e90..00000000 --- a/revocation/crl/cache/fs.go +++ /dev/null @@ -1,69 +0,0 @@ -package cache - -import ( - "context" - "os" - "path/filepath" - "time" -) - -const ( - // tempFileName is the prefix of the temporary file - tempFileName = "notation-*" -) - -// fileCache builds on top of OS file system to leverage the file system -// concurrency control and atomicity -type fileCache struct { - dir string -} - -// NewFileCache creates a new file system store -func NewFileCache(dir string, ttl time.Duration) (Cache, error) { - if err := os.MkdirAll(dir, 0700); err != nil { - return nil, err - } - - return &fileCache{dir: dir}, nil -} - -func (c *fileCache) Get(ctx context.Context, key string, maxAge time.Duration) (*Bundle, error) { - f, err := os.Open(filepath.Join(c.dir, key)) - if err != nil { - return nil, err - } - defer f.Close() - - blob, err := ParseBundleFromTarball(f) - if err != nil { - return nil, err - } - - if maxAge != 0 && time.Since(blob.Metadata.BaseCRL.CreateAt) > maxAge { - return nil, os.ErrNotExist - } - - return blob, nil -} - -func (c *fileCache) Set(ctx context.Context, key string, bundle *Bundle) error { - tempFile, err := os.CreateTemp("", tempFileName) - if err != nil { - return err - } - if err := SaveAsTarball(tempFile, bundle); err != nil { - return err - } - - tempFile.Close() - - return os.Rename(tempFile.Name(), filepath.Join(c.dir, key)) -} - -func (c *fileCache) Delete(ctx context.Context, key string) error { - return os.Remove(filepath.Join(c.dir, key)) -} - -func (c *fileCache) Clear(ctx context.Context) error { - return os.RemoveAll(c.dir) -} diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go new file mode 100644 index 00000000..acc137b2 --- /dev/null +++ b/revocation/crl/cache/memory.go @@ -0,0 +1,76 @@ +package cache + +import ( + "context" + "fmt" + "os" + "sync" + "time" +) + +// MemoryCache is an in-memory cache that stores CRL bundles. +// +// The cache is built on top of the sync.Map to leverage the concurrency control +// and atomicity of the map, so it is suitable for writing once and reading many +// times. The CRL is stored in memory as a Bundle type. +// +// MemoryCache doesn't handle cache cleaning but provides the Delete and Clear +// methods to remove the CRLs from the memory. +type MemoryCache struct { + store sync.Map + maxAge time.Duration +} + +// NewMemoryCache creates a new memory store. +// +// - maxAge is the maximum age of the CRLs cache. If the CRL is older than +// maxAge, it will be considered as expired. +func NewMemoryCache(maxAge time.Duration) *MemoryCache { + if maxAge == 0 { + maxAge = DefaultMaxAge + } + + return &MemoryCache{ + maxAge: maxAge, + } +} + +// Get retrieves the CRL from the memory store. +func (c *MemoryCache) Get(ctx context.Context, key string) (*Bundle, error) { + value, ok := c.store.Load(key) + if !ok { + return nil, os.ErrNotExist + } + + bundle, ok := value.(*Bundle) + if !ok { + return nil, fmt.Errorf("invalid type: %T", value) + } + + if c.maxAge > 0 && time.Now().After(bundle.Metadata.CreateAt.Add(c.maxAge)) { + return nil, os.ErrNotExist + } + + return bundle, nil +} + +// Set stores the CRL in the memory store. +func (c *MemoryCache) Set(ctx context.Context, key string, bundle *Bundle, expiration time.Duration) error { + c.store.Store(key, bundle) + return nil +} + +// Delete removes the CRL from the memory store. +func (c *MemoryCache) Delete(ctx context.Context, key string) error { + c.store.Delete(key) + return nil +} + +// Clear removes all CRLs from the memory store. +func (c *MemoryCache) Clear(ctx context.Context) error { + c.store.Range(func(key, value interface{}) bool { + c.store.Delete(key) + return true + }) + return nil +} diff --git a/revocation/internal/crl/fetch.go b/revocation/internal/crl/fetch.go deleted file mode 100644 index 35c64f06..00000000 --- a/revocation/internal/crl/fetch.go +++ /dev/null @@ -1,92 +0,0 @@ -package crl - -import ( - "context" - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "time" - - "github.com/notaryproject/notation-core-go/revocation/crl/cache" -) - -// maxCRLSize is the maximum size of CRL in bytes -const maxCRLSize = 64 * 1024 * 1024 // 64 MiB - -func fetch(ctx context.Context, c cache.Cache, crlURL string, cacheMaxAge time.Duration, httpClient *http.Client) (*cache.Bundle, error) { - if c == nil { - return download(ctx, crlURL, httpClient) - } - - // try to get from cache - crlBundle, err := c.Get(ctx, tarStoreName(crlURL), cacheMaxAge) - if err != nil { - var cacheBrokenError *cache.BrokenFileError - if os.IsNotExist(err) || errors.As(err, &cacheBrokenError) { - // download if not exist or broken - return download(ctx, crlURL, httpClient) - } - return nil, err - } - - return crlBundle, nil -} - -func download(ctx context.Context, crlURL string, client *http.Client) (*cache.Bundle, error) { - // validate URL - parsedURL, err := url.Parse(crlURL) - if err != nil { - return nil, fmt.Errorf("invalid CRL URL: %w", err) - } - if parsedURL.Scheme != "http" { - return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme) - } - - // download CRL - req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to create CRL request: %w", err) - } - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - // check response - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) - } - - // read with size limit - limitedReader := io.LimitReader(resp.Body, maxCRLSize) - data, err := io.ReadAll(limitedReader) - if err != nil { - return nil, fmt.Errorf("failed to read CRL response: %w", err) - } - if len(data) == maxCRLSize { - return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize) - } - - crl, err := x509.ParseRevocationList(data) - if err != nil { - return nil, fmt.Errorf("failed to parse CRL: %w", err) - } - return cache.NewBundle(crl, crlURL) -} - -func tarStoreName(url string) string { - return hashURL(url) + ".tar" -} - -// hashURL hashes the URL with SHA256 and returns the hex-encoded result -func hashURL(url string) string { - hash := sha256.Sum256([]byte(url)) - return hex.EncodeToString(hash[:]) -} diff --git a/revocation/internal/crl/fetcher.go b/revocation/internal/crl/fetcher.go new file mode 100644 index 00000000..dbe217dc --- /dev/null +++ b/revocation/internal/crl/fetcher.go @@ -0,0 +1,143 @@ +package crl + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + + "github.com/notaryproject/notation-core-go/revocation/crl/cache" +) + +// maxCRLSize is the maximum size of CRL in bytes +const maxCRLSize = 64 * 1024 * 1024 // 64 MiB + +// Fetcher is an interface that specifies methods used for fetching CRL +// from the given URL +// +// The interface is useful for pre-loading CRLs cache before the verification +type Fetcher interface { + Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) +} + +type fetcher struct { + httpClient *http.Client + cacheClient cache.Cache +} + +// NewFetcher creates a new Fetcher with the given HTTP client and cache client +// - if httpClient is nil, http.DefaultClient will be used +// - if cacheClient is nil, no cache will be used +func NewFetcher(httpClient *http.Client, cacheClient cache.Cache) Fetcher { + if httpClient == nil { + httpClient = http.DefaultClient + } + + return &fetcher{ + httpClient: httpClient, + cacheClient: cacheClient, + } +} + +// Fetch retrieves the CRL from the given URL +// - if the cache is enabled, it will try to get the CRL from the cache first +// - if the CRL is not in the cache or expired, it will download the CRL from +// the URL +func (f *fetcher) Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) { + if crlURL == "" { + return nil, false, errors.New("CRL URL is empty") + } + + if f.cacheClient == nil { + // no cache, download directly + return f.downloadAndCache(ctx, crlURL) + } + + // try to get from cache + bundle, err = f.cacheClient.Get(ctx, tarStoreName(crlURL)) + if err != nil { + var cacheBrokenError *cache.BrokenFileError + if os.IsNotExist(err) || errors.As(err, &cacheBrokenError) { + // download if not exist or broken + return f.downloadAndCache(ctx, crlURL) + } + return nil, false, err + } + + return bundle, true, nil +} + +func (f *fetcher) downloadAndCache(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) { + bundle, err = download(ctx, crlURL, f.httpClient) + if err != nil { + return nil, false, err + } + + // save to cache + if err := f.cacheClient.Set(ctx, tarStoreName(crlURL), bundle); err != nil { + return nil, false, fmt.Errorf("failed to save to cache: %w", err) + } + + return bundle, false, nil +} + +func download(ctx context.Context, crlURL string, client *http.Client) (bundle *cache.Bundle, err error) { + // validate URL + parsedURL, err := url.Parse(crlURL) + if err != nil { + return nil, fmt.Errorf("invalid CRL URL: %w", err) + } + if parsedURL.Scheme != "http" { + return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme) + } + + // download CRL + req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create CRL request: %w", err) + } + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) + } + // read with size limit + limitedReader := io.LimitReader(resp.Body, maxCRLSize) + data, err := io.ReadAll(limitedReader) + if err != nil { + return nil, fmt.Errorf("failed to read CRL response: %w", err) + } + if len(data) == maxCRLSize { + return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize) + } + + // parse CRL and create bundle + crl, err := x509.ParseRevocationList(data) + if err != nil { + return nil, fmt.Errorf("failed to parse CRL: %w", err) + } + bundle, err = cache.NewBundle(crl, crlURL) + if err != nil { + return nil, fmt.Errorf("failed to create bundle: %w", err) + } + return bundle, nil +} + +func tarStoreName(url string) string { + return hashURL(url) + ".tar" +} + +// hashURL hashes the URL with SHA256 and returns the hex-encoded result +func hashURL(url string) string { + hash := sha256.Sum256([]byte(url)) + return hex.EncodeToString(hash[:]) +} From 87d618aa31e960611113839292bcf0734b258bd8 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 8 Aug 2024 08:02:06 +0000 Subject: [PATCH 058/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/{error.go => errors.go} | 12 ++++++++ revocation/crl/cache/file.go | 30 +++++++++----------- revocation/crl/cache/memory.go | 20 ++++++------- 3 files changed, 36 insertions(+), 26 deletions(-) rename revocation/crl/cache/{error.go => errors.go} (50%) diff --git a/revocation/crl/cache/error.go b/revocation/crl/cache/errors.go similarity index 50% rename from revocation/crl/cache/error.go rename to revocation/crl/cache/errors.go index fb3853a1..9d3f6aad 100644 --- a/revocation/crl/cache/error.go +++ b/revocation/crl/cache/errors.go @@ -1,5 +1,17 @@ package cache +import "time" + +// CacheExpiredError is an error type that indicates the cache is expired. +type CacheExpiredError struct { + // Expires is the time when the cache expires. + Expires time.Time +} + +func (e *CacheExpiredError) Error() string { + return "cache expired at " + e.Expires.String() +} + // BrokenFileError is an error type for when parsing a CRL from // a tarball // diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index dd516c6c..8e7ac3c3 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -10,13 +10,9 @@ import ( const ( // tempFileName is the prefix of the temporary file tempFileName = "notation-*" - - deletedExt = ".deleted" - defaultRetryDelay = 100 * time.Millisecond - defaultRetryAttempts = 5 ) -// FileCache stores in a tarball format, which contains two files: base.crl and +// fileCache stores in a tarball format, which contains two files: base.crl and // metadata.json. The base.crl file contains the base CRL in DER format, and the // metadata.json file contains the metadata of the CRL. The cache builds on top // of UNIX file system to leverage the file system concurrency control and @@ -25,9 +21,9 @@ const ( // NOTE: For Windows, the atomicity is not guaranteed. Please avoid using this // cache on Windows when the concurrent write is required. // -// FileCache doesn't handle cache cleaning but provides the Delete and Clear +// fileCache doesn't handle cache cleaning but provides the Delete and Clear // methods to remove the CRLs from the file system. -type FileCache struct { +type fileCache struct { dir string maxAge time.Duration } @@ -42,12 +38,12 @@ type FileCacheOptions struct { // - dir is the directory to store the CRLs. // - maxAge is the maximum age of the CRLs cache. If the CRL is older than // maxAge, it will be considered as expired. -func NewFileCache(opts *FileCacheOptions) (*FileCache, error) { +func NewFileCache(opts *FileCacheOptions) (Cache, error) { if err := os.MkdirAll(opts.Dir, 0700); err != nil { return nil, err } - cache := &FileCache{ + cache := &fileCache{ dir: opts.Dir, maxAge: opts.MaxAge, } @@ -61,7 +57,7 @@ func NewFileCache(opts *FileCacheOptions) (*FileCache, error) { // // - if the key does not exist, return os.ErrNotExist // - if the CRL is expired, return os.ErrNotExist -func (c *FileCache) Get(ctx context.Context, key string) (*Bundle, error) { +func (c *fileCache) Get(ctx context.Context, key string) (*Bundle, error) { f, err := os.Open(filepath.Join(c.dir, key)) if err != nil { return nil, err @@ -73,15 +69,17 @@ func (c *FileCache) Get(ctx context.Context, key string) (*Bundle, error) { return nil, err } - if c.maxAge > 0 && time.Now().After(bundle.Metadata.CreateAt.Add(c.maxAge)) { - return nil, os.ErrNotExist + expires := bundle.Metadata.CreateAt.Add(c.maxAge) + if c.maxAge > 0 && time.Now().After(expires) { + // do not delete the file to maintain the idempotent behavior + return nil, &CacheExpiredError{Expires: expires} } return bundle, nil } // Set stores the CRL bundle in the file system -func (c *FileCache) Set(ctx context.Context, key string, bundle *Bundle, expiration time.Duration) error { +func (c *fileCache) Set(ctx context.Context, key string, bundle *Bundle) error { // save to temp file tempFile, err := os.CreateTemp("", tempFileName) if err != nil { @@ -97,12 +95,12 @@ func (c *FileCache) Set(ctx context.Context, key string, bundle *Bundle, expirat } // Delete removes the CRL bundle file from file system -func (c *FileCache) Delete(ctx context.Context, key string) error { +func (c *fileCache) Delete(ctx context.Context, key string) error { return os.Remove(filepath.Join(c.dir, key)) } -// Clear removes all CRLs from the file system -func (c *FileCache) Clear(ctx context.Context) error { +// Flush removes all CRLs from the file system +func (c *fileCache) Flush(ctx context.Context) error { return filepath.Walk(c.dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index acc137b2..bfc74b8b 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -8,15 +8,15 @@ import ( "time" ) -// MemoryCache is an in-memory cache that stores CRL bundles. +// memoryCache is an in-memory cache that stores CRL bundles. // // The cache is built on top of the sync.Map to leverage the concurrency control // and atomicity of the map, so it is suitable for writing once and reading many // times. The CRL is stored in memory as a Bundle type. // -// MemoryCache doesn't handle cache cleaning but provides the Delete and Clear +// memoryCache doesn't handle cache cleaning but provides the Delete and Clear // methods to remove the CRLs from the memory. -type MemoryCache struct { +type memoryCache struct { store sync.Map maxAge time.Duration } @@ -25,18 +25,18 @@ type MemoryCache struct { // // - maxAge is the maximum age of the CRLs cache. If the CRL is older than // maxAge, it will be considered as expired. -func NewMemoryCache(maxAge time.Duration) *MemoryCache { +func NewMemoryCache(maxAge time.Duration) Cache { if maxAge == 0 { maxAge = DefaultMaxAge } - return &MemoryCache{ + return &memoryCache{ maxAge: maxAge, } } // Get retrieves the CRL from the memory store. -func (c *MemoryCache) Get(ctx context.Context, key string) (*Bundle, error) { +func (c *memoryCache) Get(ctx context.Context, key string) (*Bundle, error) { value, ok := c.store.Load(key) if !ok { return nil, os.ErrNotExist @@ -55,19 +55,19 @@ func (c *MemoryCache) Get(ctx context.Context, key string) (*Bundle, error) { } // Set stores the CRL in the memory store. -func (c *MemoryCache) Set(ctx context.Context, key string, bundle *Bundle, expiration time.Duration) error { +func (c *memoryCache) Set(ctx context.Context, key string, bundle *Bundle) error { c.store.Store(key, bundle) return nil } // Delete removes the CRL from the memory store. -func (c *MemoryCache) Delete(ctx context.Context, key string) error { +func (c *memoryCache) Delete(ctx context.Context, key string) error { c.store.Delete(key) return nil } -// Clear removes all CRLs from the memory store. -func (c *MemoryCache) Clear(ctx context.Context) error { +// Flush removes all CRLs from the memory store. +func (c *memoryCache) Flush(ctx context.Context) error { c.store.Range(func(key, value interface{}) bool { c.store.Delete(key) return true From e128f6d981371039218157c3c52c2360e17ed010 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 9 Aug 2024 06:39:22 +0000 Subject: [PATCH 059/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/errors_test.go | 26 +++++++++ revocation/crl/cache/file.go | 4 +- revocation/crl/cache/memory.go | 5 +- .../{internal/crl => crl/fetcher}/fetcher.go | 58 +++++++++---------- 4 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 revocation/crl/cache/errors_test.go rename revocation/{internal/crl => crl/fetcher}/fetcher.go (70%) diff --git a/revocation/crl/cache/errors_test.go b/revocation/crl/cache/errors_test.go new file mode 100644 index 00000000..6981cb01 --- /dev/null +++ b/revocation/crl/cache/errors_test.go @@ -0,0 +1,26 @@ +package cache + +import ( + "errors" + "testing" + "time" +) + +func TestCacheExpiredError(t *testing.T) { + expirationTime := time.Now() + err := &CacheExpiredError{Expires: expirationTime} + + expectedMessage := "cache expired at " + expirationTime.String() + if err.Error() != expectedMessage { + t.Errorf("expected %q, got %q", expectedMessage, err.Error()) + } +} + +func TestBrokenFileError(t *testing.T) { + innerErr := errors.New("inner error") + err := &BrokenFileError{Err: innerErr} + + if err.Error() != innerErr.Error() { + t.Errorf("expected %q, got %q", innerErr.Error(), err.Error()) + } +} diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 8e7ac3c3..98f09d91 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -90,12 +90,13 @@ func (c *fileCache) Set(ctx context.Context, key string, bundle *Bundle) error { } tempFile.Close() - // rename is atomic on UNIX platforms + // rename is atomic on UNIX-like platforms return os.Rename(tempFile.Name(), filepath.Join(c.dir, key)) } // Delete removes the CRL bundle file from file system func (c *fileCache) Delete(ctx context.Context, key string) error { + // remove is atomic on UNIX-like platforms return os.Remove(filepath.Join(c.dir, key)) } @@ -109,6 +110,7 @@ func (c *fileCache) Flush(ctx context.Context) error { return nil } + // remove is atomic on UNIX-like platforms return os.Remove(path) }) } diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index bfc74b8b..fd9c512e 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -47,8 +47,9 @@ func (c *memoryCache) Get(ctx context.Context, key string) (*Bundle, error) { return nil, fmt.Errorf("invalid type: %T", value) } - if c.maxAge > 0 && time.Now().After(bundle.Metadata.CreateAt.Add(c.maxAge)) { - return nil, os.ErrNotExist + expires := bundle.Metadata.CreateAt.Add(c.maxAge) + if c.maxAge > 0 && time.Now().After(expires) { + return nil, &CacheExpiredError{Expires: expires} } return bundle, nil diff --git a/revocation/internal/crl/fetcher.go b/revocation/crl/fetcher/fetcher.go similarity index 70% rename from revocation/internal/crl/fetcher.go rename to revocation/crl/fetcher/fetcher.go index dbe217dc..52455f83 100644 --- a/revocation/internal/crl/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -1,4 +1,4 @@ -package crl +package fetcher import ( "context" @@ -20,71 +20,76 @@ const maxCRLSize = 64 * 1024 * 1024 // 64 MiB // Fetcher is an interface that specifies methods used for fetching CRL // from the given URL -// -// The interface is useful for pre-loading CRLs cache before the verification type Fetcher interface { Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) } -type fetcher struct { +type cachedFetcher struct { httpClient *http.Client cacheClient cache.Cache } -// NewFetcher creates a new Fetcher with the given HTTP client and cache client +// NewCachedFetcher creates a new Fetcher with the given HTTP client and cache client // - if httpClient is nil, http.DefaultClient will be used // - if cacheClient is nil, no cache will be used -func NewFetcher(httpClient *http.Client, cacheClient cache.Cache) Fetcher { +func NewCachedFetcher(httpClient *http.Client, cacheClient cache.Cache) (Fetcher, error) { if httpClient == nil { httpClient = http.DefaultClient } - return &fetcher{ + if cacheClient == nil { + return nil, errors.New("cache client is nil") + } + + return &cachedFetcher{ httpClient: httpClient, cacheClient: cacheClient, - } + }, nil } // Fetch retrieves the CRL from the given URL -// - if the cache is enabled, it will try to get the CRL from the cache first -// - if the CRL is not in the cache or expired, it will download the CRL from -// the URL -func (f *fetcher) Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) { +// +// Steps: +// 1. Try to get from cache +// 2. If not exist or broken, download and save to cache +func (f *cachedFetcher) Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) { if crlURL == "" { return nil, false, errors.New("CRL URL is empty") } - if f.cacheClient == nil { - // no cache, download directly - return f.downloadAndCache(ctx, crlURL) - } - // try to get from cache bundle, err = f.cacheClient.Get(ctx, tarStoreName(crlURL)) if err != nil { var cacheBrokenError *cache.BrokenFileError if os.IsNotExist(err) || errors.As(err, &cacheBrokenError) { // download if not exist or broken - return f.downloadAndCache(ctx, crlURL) + bundle, err = f.Download(ctx, crlURL) + if err != nil { + return nil, false, err + } + return bundle, false, nil } + return nil, false, err } return bundle, true, nil } -func (f *fetcher) downloadAndCache(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) { +// Download downloads the CRL from the given URL and saves it to the +// cache +func (f *cachedFetcher) Download(ctx context.Context, crlURL string) (bundle *cache.Bundle, err error) { bundle, err = download(ctx, crlURL, f.httpClient) if err != nil { - return nil, false, err + return nil, err } // save to cache if err := f.cacheClient.Set(ctx, tarStoreName(crlURL), bundle); err != nil { - return nil, false, fmt.Errorf("failed to save to cache: %w", err) + return nil, fmt.Errorf("failed to save to cache: %w", err) } - return bundle, false, nil + return bundle, nil } func download(ctx context.Context, crlURL string, client *http.Client) (bundle *cache.Bundle, err error) { @@ -111,8 +116,7 @@ func download(ctx context.Context, crlURL string, client *http.Client) (bundle * return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) } // read with size limit - limitedReader := io.LimitReader(resp.Body, maxCRLSize) - data, err := io.ReadAll(limitedReader) + data, err := io.ReadAll(io.LimitReader(resp.Body, maxCRLSize)) if err != nil { return nil, fmt.Errorf("failed to read CRL response: %w", err) } @@ -125,11 +129,7 @@ func download(ctx context.Context, crlURL string, client *http.Client) (bundle * if err != nil { return nil, fmt.Errorf("failed to parse CRL: %w", err) } - bundle, err = cache.NewBundle(crl, crlURL) - if err != nil { - return nil, fmt.Errorf("failed to create bundle: %w", err) - } - return bundle, nil + return cache.NewBundle(crl, crlURL) } func tarStoreName(url string) string { From 0caff0bb2410a519d731c11ffde2217875a7406f Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 9 Aug 2024 07:51:02 +0000 Subject: [PATCH 060/110] test: add unit test for bundle.go Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 62 +++--- revocation/crl/cache/bundle_test.go | 306 ++++++++++++++++++++++++++++ revocation/crl/cache/file.go | 2 +- testhelper/certificatetest.go | 49 ++++- 4 files changed, 377 insertions(+), 42 deletions(-) create mode 100644 revocation/crl/cache/bundle_test.go diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index b779386d..a831b341 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -64,12 +64,12 @@ func NewBundle(baseCRL *x509.RevocationList, url string) (*Bundle, error) { // // { // "base.crl": { -// "url": "https://example.com/base.crl", -// "createAt": "2021-09-01T00:00:00Z" -// } +// "url": "https://example.com/base.crl" +// }, +// "createAt": "2024-07-20T00:00:00Z" // } func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { - crl := &Bundle{} + bundle := &Bundle{} // parse the tarball tar := tar.NewReader(data) @@ -99,8 +99,7 @@ func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { Err: fmt.Errorf("failed to parse base CRL from tarball: %w", err), } } - - crl.BaseCRL = baseCRL + bundle.BaseCRL = baseCRL case PathMetadata: // parse metadata var metadata Metadata @@ -109,34 +108,31 @@ func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { Err: fmt.Errorf("failed to parse CRL metadata from tarball: %w", err), } } - - crl.Metadata = metadata - + bundle.Metadata = metadata default: return nil, &BrokenFileError{ Err: fmt.Errorf("unexpected file in CRL tarball: %s", header.Name), } } } + if err := bundle.validate(); err != nil { + return nil, err + } - // validate - if crl.BaseCRL == nil { - return nil, &BrokenFileError{ - Err: errors.New("base CRL is missing from cached tarball"), - } + return bundle, nil +} + +func (b *Bundle) validate() error { + if b.BaseCRL == nil { + return errors.New("base CRL is missing") } - if crl.Metadata.BaseCRL.URL == "" { - return nil, &BrokenFileError{ - Err: errors.New("base CRL URL is missing from cached tarball"), - } + if b.Metadata.BaseCRL.URL == "" { + return errors.New("base CRL URL is missing") } - if crl.Metadata.CreateAt.IsZero() { - return nil, &BrokenFileError{ - Err: errors.New("base CRL creation time is missing from cached tarball"), - } + if b.Metadata.CreateAt.IsZero() { + return errors.New("base CRL creation time is missing") } - - return crl, nil + return nil } // SaveAsTar saves the CRL blob as a tarball, including the base CRL and @@ -150,19 +146,25 @@ func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { // // { // "base.crl": { -// "url": "https://example.com/base.crl", -// "createAt": "2021-09-01T00:00:00Z" -// } +// "url": "https://example.com/base.crl" +// }, +// "createAt": "2024-06-30T00:00:00Z" // } -func SaveAsTarball(w io.Writer, bundle *Bundle) (err error) { +func (b *Bundle) SaveAsTarball(w io.Writer) (err error) { + if err := b.validate(); err != nil { + return err + } + tarWriter := tar.NewWriter(w) + defer tarWriter.Close() + // Add base.crl - if err := addToTar(PathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CreateAt, tarWriter); err != nil { + if err := addToTar(PathBaseCRL, b.BaseCRL.Raw, b.Metadata.CreateAt, tarWriter); err != nil { return err } // Add metadata.json - metadataBytes, err := json.Marshal(bundle.Metadata) + metadataBytes, err := json.Marshal(b.Metadata) if err != nil { return err } diff --git a/revocation/crl/cache/bundle_test.go b/revocation/crl/cache/bundle_test.go new file mode 100644 index 00000000..732def42 --- /dev/null +++ b/revocation/crl/cache/bundle_test.go @@ -0,0 +1,306 @@ +package cache + +import ( + "archive/tar" + "bytes" + "crypto/rand" + "crypto/x509" + "math/big" + "os" + "testing" + "time" + + "github.com/notaryproject/notation-core-go/testhelper" +) + +func TestNewBundle(t *testing.T) { + baseCRL := &x509.RevocationList{} + url := "https://example.com/base.crl" + bundle, err := NewBundle(baseCRL, url) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if bundle.BaseCRL != baseCRL { + t.Errorf("expected BaseCRL to be %v, got %v", baseCRL, bundle.BaseCRL) + } + + if bundle.Metadata.BaseCRL.URL != url { + t.Errorf("expected URL to be %s, got %s", url, bundle.Metadata.BaseCRL.URL) + } + + if bundle.Metadata.CreateAt.IsZero() { + t.Errorf("expected CreateAt to be set, got zero value") + } +} + +func TestBundle(t *testing.T) { + const exampleURL = "https://example.com/base.crl" + var buf bytes.Buffer + + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + t.Run("SaveAsTarball", func(t *testing.T) { + // Create a tarball + baseCRL, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } + bundle, err := NewBundle(baseCRL, exampleURL) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if err := bundle.SaveAsTarball(&buf); err != nil { + t.Fatalf("expected no error, got %v", err) + } + }) + + t.Run("ParseBundleFromTarball", func(t *testing.T) { + // Parse the tarball + bundle, err := ParseBundleFromTarball(&buf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if !bytes.Equal(crlBytes, bundle.BaseCRL.Raw) { + t.Errorf("expected BaseCRL to be %v, got %v", crlBytes, bundle.BaseCRL.Raw) + } + + if bundle.Metadata.BaseCRL.URL != exampleURL { + t.Errorf("expected URL to be %s, got %s", exampleURL, bundle.Metadata.BaseCRL.URL) + } + + if bundle.Metadata.CreateAt.IsZero() { + t.Errorf("expected CreateAt to be set, got zero value") + } + }) +} + +func TestBundleParseFailed(t *testing.T) { + t.Run("IO read error", func(t *testing.T) { + _, err := ParseBundleFromTarball(&errorReader{}) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("missing baseCRL content (only has baseCRL header in tarball)", func(t *testing.T) { + var buf bytes.Buffer + header := &tar.Header{ + Name: "base.crl", + Size: 10, + Mode: 0644, + ModTime: time.Now(), + } + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Close() + + _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("broken baseCRL", func(t *testing.T) { + var buf bytes.Buffer + header := &tar.Header{ + Name: "base.crl", + Size: 10, + Mode: 0644, + ModTime: time.Now(), + } + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write([]byte("broken crl")) + tw.Close() + + _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("malformed metadata", func(t *testing.T) { + var buf bytes.Buffer + header := &tar.Header{ + Name: "metadata.json", + Size: 10, + Mode: 0644, + ModTime: time.Now(), + } + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write([]byte("malformed json")) + tw.Close() + + _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("unknown file in tarball", func(t *testing.T) { + var buf bytes.Buffer + header := &tar.Header{ + Name: "unknown file", + Size: 10, + Mode: 0644, + ModTime: time.Now(), + } + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write([]byte("unknown file")) + tw.Close() + + _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) +} + +func TestValidate(t *testing.T) { + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + + t.Run("missing BaseCRL", func(t *testing.T) { + var buf bytes.Buffer + _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("missing metadata baseCRL URL", func(t *testing.T) { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + baseCRLHeader := &tar.Header{ + Name: "base.crl", + Size: int64(len(crlBytes)), + Mode: 0644, + ModTime: time.Now(), + } + if err := tw.WriteHeader(baseCRLHeader); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write(crlBytes) + + metadataContent := []byte(`{"base.crl": {}}`) + metadataHeader := &tar.Header{ + Name: "metadata.json", + Size: int64(len(metadataContent)), + Mode: 0644, + ModTime: time.Now(), + } + if err := tw.WriteHeader(metadataHeader); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write(metadataContent) + tw.Close() + + _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("missing metadata createAt", func(t *testing.T) { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + baseCRLHeader := &tar.Header{ + Name: "base.crl", + Size: int64(len(crlBytes)), + Mode: 0644, + ModTime: time.Now(), + } + if err := tw.WriteHeader(baseCRLHeader); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write(crlBytes) + + metadataContent := []byte(`{"base.crl": {"url": "https://example.com/base.crl"}}`) + metadataHeader := &tar.Header{ + Name: "metadata.json", + Size: int64(len(metadataContent)), + Mode: 0644, + ModTime: time.Now(), + } + if err := tw.WriteHeader(metadataHeader); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write(metadataContent) + tw.Close() + + _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) +} + +func TestSaveAsTarballFailed(t *testing.T) { + t.Run("validate failed", func(t *testing.T) { + bundle := &Bundle{} + if err := bundle.SaveAsTarball(&errorWriter{}); err == nil { + t.Fatalf("expected error, got nil") + } + }) + + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + + t.Run("write base CRL to tarball failed", func(t *testing.T) { + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } + bundle, err := NewBundle(crl, "https://example.com/base.crl") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if err := bundle.SaveAsTarball(&errorWriter{}); err == nil { + t.Fatalf("expected error, got nil") + } + }) +} + +type errorReader struct{} + +func (r *errorReader) Read(p []byte) (n int, err error) { + return 0, os.ErrNotExist +} + +type errorWriter struct { + Errors []error + i int +} + +func (w *errorWriter) Write(p []byte) (n int, err error) { + return 0, os.ErrNotExist +} diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 98f09d91..d7db28c0 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -85,7 +85,7 @@ func (c *fileCache) Set(ctx context.Context, key string, bundle *Bundle) error { if err != nil { return err } - if err := SaveAsTarball(tempFile, bundle); err != nil { + if err := bundle.SaveAsTarball(tempFile); err != nil { return err } tempFile.Close() diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index 54a31c0e..e1e3d7e4 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -75,16 +75,29 @@ func GetRevokableRSALeafCertificate() RSACertTuple { return revokableRSALeaf } +func GetRevokableRSAChainWithRevocations(size int, enabledOCSP, enabledCRL bool) []RSACertTuple { + setupCertificates() + chain := make([]RSACertTuple, size) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, enabledCRL) + for i := size - 2; i > 0; i-- { + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, enabledOCSP, enabledCRL) + } + if size > 1 { + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false, enabledOCSP, enabledCRL) + } + return chain +} + // GetRevokableRSAChain returns a chain of certificates that specify a local OCSP server signed using RSA algorithm func GetRevokableRSAChain(size int) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) - chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, false) for i := size - 2; i > 0; i-- { - chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, true, false) } if size > 1 { - chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false) + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false, true, false) } return chain } @@ -94,12 +107,12 @@ func GetRevokableRSAChain(size int) []RSACertTuple { func GetRevokableRSATimestampChain(size int) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) - chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, false) for i := size - 2; i > 0; i-- { - chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, true, false) } if size > 1 { - chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true) + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true, true, false) } return chain } @@ -171,31 +184,45 @@ func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } -func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) RSACertTuple { +func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int, enabledOCSP, enabledCRL bool) RSACertTuple { template := getCertTemplate(previous == nil, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign - template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} + if enabledOCSP { + template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} + } + if enabledCRL { + template.KeyUsage |= x509.KeyUsageCRLSign + template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d", index)} + } return getRSACertTupleWithTemplate(template, previous.PrivateKey, previous) } -func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { +func getRevokableRSARootChainCertTuple(cn string, pathLen int, enabledCRL bool) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) template := getCertTemplate(true, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign + if enabledCRL { + template.KeyUsage |= x509.KeyUsageCRLSign + } template.MaxPathLen = pathLen return getRSACertTupleWithTemplate(template, pk, nil) } -func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp bool) RSACertTuple { +func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp, enabledOCSP, enabledCRL bool) RSACertTuple { template := getCertTemplate(false, codesign, timestamp, cn) template.BasicConstraintsValid = true template.IsCA = false template.KeyUsage = x509.KeyUsageDigitalSignature - template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} + if enabledOCSP { + template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} + } + if enabledCRL { + template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d", index)} + } return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } From 51ef3bfae899f8452344f41bab9cc623eb8a9530 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 9 Aug 2024 07:59:40 +0000 Subject: [PATCH 061/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle_test.go | 2 -- revocation/crl/cache/memory.go | 16 +++++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/revocation/crl/cache/bundle_test.go b/revocation/crl/cache/bundle_test.go index 732def42..e8181856 100644 --- a/revocation/crl/cache/bundle_test.go +++ b/revocation/crl/cache/bundle_test.go @@ -297,8 +297,6 @@ func (r *errorReader) Read(p []byte) (n int, err error) { } type errorWriter struct { - Errors []error - i int } func (w *errorWriter) Write(p []byte) (n int, err error) { diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index fd9c512e..acf031e7 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -21,18 +21,24 @@ type memoryCache struct { maxAge time.Duration } +type MemoryCacheOptions struct { + MaxAge time.Duration +} + // NewMemoryCache creates a new memory store. // // - maxAge is the maximum age of the CRLs cache. If the CRL is older than // maxAge, it will be considered as expired. -func NewMemoryCache(maxAge time.Duration) Cache { - if maxAge == 0 { - maxAge = DefaultMaxAge +func NewMemoryCache(opts MemoryCacheOptions) (Cache, error) { + c := &memoryCache{ + maxAge: opts.MaxAge, } - return &memoryCache{ - maxAge: maxAge, + if c.maxAge == 0 { + c.maxAge = DefaultMaxAge } + + return c, nil } // Get retrieves the CRL from the memory store. From c7a54cd13c60c2a8f242688ee092f95c1516bcdc Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 9 Aug 2024 08:04:21 +0000 Subject: [PATCH 062/110] test: add unit test for memory cache Signed-off-by: Junjie Gao --- revocation/crl/cache/memory_test.go | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 revocation/crl/cache/memory_test.go diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go new file mode 100644 index 00000000..d01b40cb --- /dev/null +++ b/revocation/crl/cache/memory_test.go @@ -0,0 +1,88 @@ +package cache + +import ( + "context" + "testing" + "time" +) + +func TestMemoryCache(t *testing.T) { + ctx := context.Background() + + // Test NewMemoryCache + opts := MemoryCacheOptions{MaxAge: 5 * time.Minute} + cache, err := NewMemoryCache(opts) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if cache.(*memoryCache).maxAge != opts.MaxAge { + t.Fatalf("expected maxAge %v, got %v", opts.MaxAge, cache.(*memoryCache).maxAge) + } + + // Test Set and Get + bundle := &Bundle{Metadata: Metadata{CreateAt: time.Now()}} + key := "testKey" + if err := cache.Set(ctx, key, bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + retrievedBundle, err := cache.Get(ctx, key) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if retrievedBundle != bundle { + t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) + } + + // Test Get with expired bundle + expiredBundle := &Bundle{Metadata: Metadata{CreateAt: time.Now().Add(-10 * time.Minute)}} + if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = cache.Get(ctx, "expiredKey") + if _, ok := err.(*CacheExpiredError); !ok { + t.Fatalf("expected CacheExpiredError, got %v", err) + } + + // Test Delete + if err := cache.Delete(ctx, key); err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = cache.Get(ctx, key) + if err == nil { + t.Fatalf("expected error, got nil") + } + + // Test Flush + if err := cache.Set(ctx, "key1", bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if err := cache.Set(ctx, "key2", bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if err := cache.Flush(ctx); err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = cache.Get(ctx, "key1") + if err == nil { + t.Fatalf("expected error, got nil") + } + _, err = cache.Get(ctx, "key2") + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +func TestMemoryCacheFailed(t *testing.T) { + ctx := context.Background() + + // Test Get with invalid type + cache, err := NewMemoryCache(MemoryCacheOptions{}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + cache.(*memoryCache).store.Store("invalidKey", "invalidValue") + _, err = cache.Get(ctx, "invalidKey") + if err == nil { + t.Fatalf("expected error, got nil") + } +} From 20b8189b8f93d74c9f1602514557c71d9b33f009 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 9 Aug 2024 09:02:10 +0000 Subject: [PATCH 063/110] test: update Signed-off-by: Junjie Gao --- revocation/crl/cache/errors.go | 16 ++- revocation/crl/cache/errors_test.go | 2 +- revocation/crl/cache/file.go | 8 +- revocation/crl/cache/file_test.go | 202 ++++++++++++++++++++++++++++ revocation/crl/cache/memory.go | 5 +- revocation/crl/cache/memory_test.go | 96 ++++++------- 6 files changed, 274 insertions(+), 55 deletions(-) create mode 100644 revocation/crl/cache/file_test.go diff --git a/revocation/crl/cache/errors.go b/revocation/crl/cache/errors.go index 9d3f6aad..44047fa4 100644 --- a/revocation/crl/cache/errors.go +++ b/revocation/crl/cache/errors.go @@ -2,13 +2,23 @@ package cache import "time" -// CacheExpiredError is an error type that indicates the cache is expired. -type CacheExpiredError struct { +// NotExistError is an error type that indicates the key is not found in the +// cache. +type NotExistError struct { + Key string +} + +func (e *NotExistError) Error() string { + return "key not found: " + e.Key +} + +// ExpiredError is an error type that indicates the cache is expired. +type ExpiredError struct { // Expires is the time when the cache expires. Expires time.Time } -func (e *CacheExpiredError) Error() string { +func (e *ExpiredError) Error() string { return "cache expired at " + e.Expires.String() } diff --git a/revocation/crl/cache/errors_test.go b/revocation/crl/cache/errors_test.go index 6981cb01..abfa19a9 100644 --- a/revocation/crl/cache/errors_test.go +++ b/revocation/crl/cache/errors_test.go @@ -8,7 +8,7 @@ import ( func TestCacheExpiredError(t *testing.T) { expirationTime := time.Now() - err := &CacheExpiredError{Expires: expirationTime} + err := &ExpiredError{Expires: expirationTime} expectedMessage := "cache expired at " + expirationTime.String() if err.Error() != expectedMessage { diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index d7db28c0..4ed6a53b 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -2,6 +2,7 @@ package cache import ( "context" + "fmt" "os" "path/filepath" "time" @@ -40,7 +41,7 @@ type FileCacheOptions struct { // maxAge, it will be considered as expired. func NewFileCache(opts *FileCacheOptions) (Cache, error) { if err := os.MkdirAll(opts.Dir, 0700); err != nil { - return nil, err + return nil, fmt.Errorf("failed to create directory: %w", err) } cache := &fileCache{ @@ -60,6 +61,9 @@ func NewFileCache(opts *FileCacheOptions) (Cache, error) { func (c *fileCache) Get(ctx context.Context, key string) (*Bundle, error) { f, err := os.Open(filepath.Join(c.dir, key)) if err != nil { + if os.IsNotExist(err) { + return nil, &NotExistError{Key: key} + } return nil, err } defer f.Close() @@ -72,7 +76,7 @@ func (c *fileCache) Get(ctx context.Context, key string) (*Bundle, error) { expires := bundle.Metadata.CreateAt.Add(c.maxAge) if c.maxAge > 0 && time.Now().After(expires) { // do not delete the file to maintain the idempotent behavior - return nil, &CacheExpiredError{Expires: expires} + return nil, &ExpiredError{Expires: expires} } return bundle, nil diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go new file mode 100644 index 00000000..5d12d2dc --- /dev/null +++ b/revocation/crl/cache/file_test.go @@ -0,0 +1,202 @@ +package cache + +import ( + "context" + "crypto/rand" + "crypto/x509" + "errors" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/notaryproject/notation-core-go/testhelper" +) + +func TestFileCache(t *testing.T) { + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + baseCRL, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } + + ctx := context.Background() + dir := t.TempDir() + opts := &FileCacheOptions{Dir: dir, MaxAge: 5 * time.Minute} + cache, err := NewFileCache(opts) + t.Run("NewFileCache", func(t *testing.T) { + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if cache.(*fileCache).dir != opts.Dir { + t.Fatalf("expected dir %v, got %v", opts.Dir, cache.(*fileCache).dir) + } + if cache.(*fileCache).maxAge != opts.MaxAge { + t.Fatalf("expected maxAge %v, got %v", opts.MaxAge, cache.(*fileCache).maxAge) + } + }) + + key := "testKey" + bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: FileInfo{URL: "http://crl"}, CreateAt: time.Now()}} + t.Run("SetAndGet", func(t *testing.T) { + if err := cache.Set(ctx, key, bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + retrievedBundle, err := cache.Get(ctx, key) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if retrievedBundle.Metadata.CreateAt.Unix() != bundle.Metadata.CreateAt.Unix() { + t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) + } + }) + + t.Run("GetWithExpiredBundle", func(t *testing.T) { + expiredBundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: FileInfo{URL: "http://crl"}, CreateAt: time.Now().Add(-10 * time.Minute)}} + if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = cache.Get(ctx, "expiredKey") + if _, ok := err.(*ExpiredError); !ok { + t.Fatalf("expected CacheExpiredError, got %v", err) + } + }) + + t.Run("Delete", func(t *testing.T) { + // Test Delete + if err := cache.Delete(ctx, key); err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = cache.Get(ctx, key) + var notExistError *NotExistError + if !errors.As(err, ¬ExistError) { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("Flush", func(t *testing.T) { + if err := cache.Set(ctx, "key1", bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if err := cache.Set(ctx, "key2", bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if err := cache.Flush(ctx); err != nil { + t.Fatalf("expected no error, got %v", err) + } + var notExistError *NotExistError + _, err = cache.Get(ctx, "key1") + if !errors.As(err, ¬ExistError) { + t.Fatalf("expected error, got nil") + } + _, err = cache.Get(ctx, "key2") + if !errors.As(err, ¬ExistError) { + t.Fatalf("expected error, got nil") + } + }) +} + +func TestNewFileCache(t *testing.T) { + tempDir := t.TempDir() + t.Run("without permission to create cache directory", func(t *testing.T) { + if err := os.Chmod(tempDir, 0); err != nil { + t.Fatalf("failed to change permission: %v", err) + } + _, err := NewFileCache(&FileCacheOptions{Dir: filepath.Join(tempDir, "test")}) + if err == nil { + t.Fatalf("expected error, got nil") + } + // restore permission + if err := os.Chmod(tempDir, 0755); err != nil { + t.Fatalf("failed to change permission: %v", err) + } + }) + + t.Run("no maxAge", func(t *testing.T) { + cache, err := NewFileCache(&FileCacheOptions{Dir: tempDir}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if cache.(*fileCache).maxAge != DefaultMaxAge { + t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.(*fileCache).maxAge) + } + }) +} + +func TestGetFailed(t *testing.T) { + tempDir := t.TempDir() + // write an invalid tarball + invalidTarball := filepath.Join(tempDir, "invalid.tar") + if err := os.WriteFile(invalidTarball, []byte("invalid tarball"), 0644); err != nil { + t.Fatalf("failed to write file: %v", err) + } + + cache, err := NewFileCache(&FileCacheOptions{Dir: tempDir}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + t.Run("invalid tarball", func(t *testing.T) { + _, err := cache.Get(context.Background(), "invalid.tar") + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("no permission to read file", func(t *testing.T) { + if err := os.Chmod(tempDir, 0); err != nil { + t.Fatalf("failed to change permission: %v", err) + } + _, err := cache.Get(context.Background(), "invalid.tar") + if err == nil { + t.Fatalf("expected error, got nil") + } + // restore permission + if err := os.Chmod(tempDir, 0755); err != nil { + t.Fatalf("failed to change permission: %v", err) + } + }) +} + +func TestSetFailed(t *testing.T) { + tempDir := t.TempDir() + cache, err := NewFileCache(&FileCacheOptions{Dir: tempDir}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + t.Run("failed to save tarball", func(t *testing.T) { + bundle := &Bundle{Metadata: Metadata{CreateAt: time.Now()}} + if err := cache.Set(context.Background(), "invalid.tar", bundle); err == nil { + t.Fatalf("expected error, got nil") + } + }) +} + +func TestFlushFailed(t *testing.T) { + tempDir := t.TempDir() + cache, err := NewFileCache(&FileCacheOptions{Dir: tempDir}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + t.Run("failed to remove files", func(t *testing.T) { + if err := os.Chmod(tempDir, 0); err != nil { + t.Fatalf("failed to change permission: %v", err) + } + if err := cache.Flush(context.Background()); err == nil { + t.Fatalf("expected error, got nil") + } + // restore permission + if err := os.Chmod(tempDir, 0755); err != nil { + t.Fatalf("failed to change permission: %v", err) + } + }) +} diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index acf031e7..b91173dc 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -3,7 +3,6 @@ package cache import ( "context" "fmt" - "os" "sync" "time" ) @@ -45,7 +44,7 @@ func NewMemoryCache(opts MemoryCacheOptions) (Cache, error) { func (c *memoryCache) Get(ctx context.Context, key string) (*Bundle, error) { value, ok := c.store.Load(key) if !ok { - return nil, os.ErrNotExist + return nil, &NotExistError{Key: key} } bundle, ok := value.(*Bundle) @@ -55,7 +54,7 @@ func (c *memoryCache) Get(ctx context.Context, key string) (*Bundle, error) { expires := bundle.Metadata.CreateAt.Add(c.maxAge) if c.maxAge > 0 && time.Now().After(expires) { - return nil, &CacheExpiredError{Expires: expires} + return nil, &ExpiredError{Expires: expires} } return bundle, nil diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index d01b40cb..fb924041 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -19,57 +19,61 @@ func TestMemoryCache(t *testing.T) { t.Fatalf("expected maxAge %v, got %v", opts.MaxAge, cache.(*memoryCache).maxAge) } - // Test Set and Get bundle := &Bundle{Metadata: Metadata{CreateAt: time.Now()}} key := "testKey" - if err := cache.Set(ctx, key, bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - retrievedBundle, err := cache.Get(ctx, key) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - if retrievedBundle != bundle { - t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) - } + t.Run("SetAndGet", func(t *testing.T) { + if err := cache.Set(ctx, key, bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + retrievedBundle, err := cache.Get(ctx, key) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if retrievedBundle != bundle { + t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) + } + }) - // Test Get with expired bundle - expiredBundle := &Bundle{Metadata: Metadata{CreateAt: time.Now().Add(-10 * time.Minute)}} - if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = cache.Get(ctx, "expiredKey") - if _, ok := err.(*CacheExpiredError); !ok { - t.Fatalf("expected CacheExpiredError, got %v", err) - } + t.Run("GetWithExpiredBundle", func(t *testing.T) { + expiredBundle := &Bundle{Metadata: Metadata{CreateAt: time.Now().Add(-10 * time.Minute)}} + if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = cache.Get(ctx, "expiredKey") + if _, ok := err.(*ExpiredError); !ok { + t.Fatalf("expected CacheExpiredError, got %v", err) + } + }) - // Test Delete - if err := cache.Delete(ctx, key); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = cache.Get(ctx, key) - if err == nil { - t.Fatalf("expected error, got nil") - } + t.Run("Delete", func(t *testing.T) { + if err := cache.Delete(ctx, key); err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = cache.Get(ctx, key) + if _, ok := err.(*NotExistError); !ok { + t.Fatalf("expected error, got nil") + } + }) - // Test Flush - if err := cache.Set(ctx, "key1", bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - if err := cache.Set(ctx, "key2", bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - if err := cache.Flush(ctx); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = cache.Get(ctx, "key1") - if err == nil { - t.Fatalf("expected error, got nil") - } - _, err = cache.Get(ctx, "key2") - if err == nil { - t.Fatalf("expected error, got nil") - } + t.Run("Flush", func(t *testing.T) { + if err := cache.Set(ctx, "key1", bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if err := cache.Set(ctx, "key2", bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if err := cache.Flush(ctx); err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = cache.Get(ctx, "key1") + if _, ok := err.(*NotExistError); !ok { + t.Fatalf("expected error, got nil") + } + _, err = cache.Get(ctx, "key2") + if _, ok := err.(*NotExistError); !ok { + t.Fatalf("expected error, got nil") + } + }) } func TestMemoryCacheFailed(t *testing.T) { From 7300ffd5e28d27818c6dd7108f066cba8995b196 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 9 Aug 2024 09:04:45 +0000 Subject: [PATCH 064/110] fix: update license Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 13 +++++++++++++ revocation/crl/cache/bundle_test.go | 13 +++++++++++++ revocation/crl/cache/cache.go | 13 +++++++++++++ revocation/crl/cache/errors.go | 13 +++++++++++++ revocation/crl/cache/errors_test.go | 13 +++++++++++++ revocation/crl/cache/file.go | 13 +++++++++++++ revocation/crl/cache/file_test.go | 13 +++++++++++++ revocation/crl/cache/memory.go | 13 +++++++++++++ revocation/crl/cache/memory_test.go | 13 +++++++++++++ revocation/crl/fetcher/fetcher.go | 13 +++++++++++++ 10 files changed, 130 insertions(+) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index a831b341..d53eaca7 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -1,3 +1,16 @@ +// 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 cache import ( diff --git a/revocation/crl/cache/bundle_test.go b/revocation/crl/cache/bundle_test.go index e8181856..1fcb2332 100644 --- a/revocation/crl/cache/bundle_test.go +++ b/revocation/crl/cache/bundle_test.go @@ -1,3 +1,16 @@ +// 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 cache import ( diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index 46669b10..a86ff3f2 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -1,3 +1,16 @@ +// 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 cache provides methods for caching CRL // // The fileSystemCache is an implementation of the Cache interface that uses the diff --git a/revocation/crl/cache/errors.go b/revocation/crl/cache/errors.go index 44047fa4..8c72cdbf 100644 --- a/revocation/crl/cache/errors.go +++ b/revocation/crl/cache/errors.go @@ -1,3 +1,16 @@ +// 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 cache import "time" diff --git a/revocation/crl/cache/errors_test.go b/revocation/crl/cache/errors_test.go index abfa19a9..576affe7 100644 --- a/revocation/crl/cache/errors_test.go +++ b/revocation/crl/cache/errors_test.go @@ -1,3 +1,16 @@ +// 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 cache import ( diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 4ed6a53b..ca16580a 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -1,3 +1,16 @@ +// 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 cache import ( diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index 5d12d2dc..8bd826cf 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -1,3 +1,16 @@ +// 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 cache import ( diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index b91173dc..f3a73da2 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -1,3 +1,16 @@ +// 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 cache import ( diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index fb924041..3a60ed7a 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -1,3 +1,16 @@ +// 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 cache import ( diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher/fetcher.go index 52455f83..5f41ee49 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -1,3 +1,16 @@ +// 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 fetcher import ( From 0d5c99fcf5c1c0a5add34fedef33a29d9f59c83e Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 9 Aug 2024 09:37:22 +0000 Subject: [PATCH 065/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher/fetcher.go | 6 +- revocation/crl/fetcher/fetcher_test.go | 274 +++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 revocation/crl/fetcher/fetcher_test.go diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher/fetcher.go index 5f41ee49..798e63e5 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package fetcher provides Fetcher interface and its implementation to fetch +// CRL from the given URL package fetcher import ( @@ -23,7 +25,6 @@ import ( "io" "net/http" "net/url" - "os" "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) @@ -73,8 +74,9 @@ func (f *cachedFetcher) Fetch(ctx context.Context, crlURL string) (bundle *cache // try to get from cache bundle, err = f.cacheClient.Get(ctx, tarStoreName(crlURL)) if err != nil { + var notExistError *cache.NotExistError var cacheBrokenError *cache.BrokenFileError - if os.IsNotExist(err) || errors.As(err, &cacheBrokenError) { + if errors.As(err, ¬ExistError) || errors.As(err, &cacheBrokenError) { // download if not exist or broken bundle, err = f.Download(ctx, crlURL) if err != nil { diff --git a/revocation/crl/fetcher/fetcher_test.go b/revocation/crl/fetcher/fetcher_test.go new file mode 100644 index 00000000..0b6677a5 --- /dev/null +++ b/revocation/crl/fetcher/fetcher_test.go @@ -0,0 +1,274 @@ +// 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 fetcher + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/x509" + "fmt" + "io" + "math/big" + "net/http" + "testing" + + "github.com/notaryproject/notation-core-go/revocation/crl/cache" + "github.com/notaryproject/notation-core-go/testhelper" +) + +func TestNewCachedFetcher(t *testing.T) { + c, err := cache.NewMemoryCache(cache.MemoryCacheOptions{}) + if err != nil { + t.Errorf("NewMemoryCache() error = %v, want nil", err) + } + t.Run("httpClient is nil", func(t *testing.T) { + _, err := NewCachedFetcher(nil, c) + if err != nil { + t.Errorf("NewCachedFetcher() error = %v, want nil", err) + } + }) + + t.Run("cacheClient is nil", func(t *testing.T) { + _, err := NewCachedFetcher(nil, nil) + if err == nil { + t.Errorf("NewCachedFetcher() error = nil, want not nil") + } + }) +} + +func TestFetch(t *testing.T) { + // prepare cache + c, err := cache.NewMemoryCache(cache.MemoryCacheOptions{}) + if err != nil { + t.Errorf("NewMemoryCache() error = %v, want nil", err) + } + + // prepare crl + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + baseCRL, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } + const exampleURL = "http://example.com" + const uncachedURL = "http://uncached.com" + + bundle, err := cache.NewBundle(baseCRL, exampleURL) + if err != nil { + t.Errorf("NewBundle() error = %v, want nil", err) + } + if err := c.Set(context.Background(), tarStoreName(exampleURL), bundle); err != nil { + t.Errorf("Cache.Set() error = %v, want nil", err) + } + + t.Run("url is empty", func(t *testing.T) { + f, err := NewCachedFetcher(nil, c) + if err != nil { + t.Errorf("NewCachedFetcher() error = %v, want nil", err) + } + _, _, err = f.Fetch(context.Background(), "") + if err == nil { + t.Errorf("Fetcher.Fetch() error = nil, want not nil") + } + }) + + t.Run("cache hit", func(t *testing.T) { + f, err := NewCachedFetcher(nil, c) + if err != nil { + t.Errorf("NewCachedFetcher() error = %v, want nil", err) + } + fetchedBundle, fromCache, err := f.Fetch(context.Background(), exampleURL) + if err != nil { + t.Errorf("Fetcher.Fetch() error = %v, want nil", err) + } + if !fromCache { + t.Errorf("Fetcher.Fetch() fromCache = false, want true") + } + if fetchedBundle == nil { + t.Errorf("Fetcher.Fetch() fetchedBundle = nil, want not nil") + } + if fetchedBundle != nil && fetchedBundle.Metadata.BaseCRL.URL != exampleURL { + t.Errorf("Fetcher.Fetch() fetchedBundle.Metadata.BaseCRL.URL = %v, want %v", fetchedBundle.Metadata.BaseCRL.URL, exampleURL) + } + if !bytes.Equal(fetchedBundle.BaseCRL.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() fetchedBundle.BaseCRL.Raw = %v, want %v", fetchedBundle.BaseCRL.Raw, baseCRL.Raw) + } + }) + + t.Run("cache miss", func(t *testing.T) { + httpClient := &http.Client{ + Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, + } + f, err := NewCachedFetcher(httpClient, c) + if err != nil { + t.Errorf("NewCachedFetcher() error = %v, want nil", err) + } + fetchedBundle, fromCache, err := f.Fetch(context.Background(), uncachedURL) + if err != nil { + t.Errorf("Fetcher.Fetch() error = %v, want nil", err) + } + if fromCache { + t.Errorf("Fetcher.Fetch() fromCache = true, want false") + } + if fetchedBundle == nil { + t.Errorf("Fetcher.Fetch() fetchedBundle = nil, want not nil") + } + if fetchedBundle != nil && fetchedBundle.Metadata.BaseCRL.URL != uncachedURL { + t.Errorf("Fetcher.Fetch() fetchedBundle.Metadata.BaseCRL.URL = %v, want %v", fetchedBundle.Metadata.BaseCRL.URL, exampleURL) + } + if !bytes.Equal(fetchedBundle.BaseCRL.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() fetchedBundle.BaseCRL.Raw = %v, want %v", fetchedBundle.BaseCRL.Raw, baseCRL.Raw) + } + + // delete cache + if err := c.Delete(context.Background(), tarStoreName(uncachedURL)); err != nil { + t.Errorf("Cache.Delete() error = %v, want nil", err) + } + }) + + t.Run("cache miss and download failed error", func(t *testing.T) { + httpClient := &http.Client{ + Transport: errorRoundTripperMock{}, + } + f, err := NewCachedFetcher(httpClient, c) + if err != nil { + t.Errorf("NewCachedFetcher() error = %v, want nil", err) + } + _, _, err = f.Fetch(context.Background(), uncachedURL) + if err == nil { + t.Errorf("Fetcher.Fetch() error = nil, want not nil") + } + }) +} + +func TestDownload(t *testing.T) { + t.Run("parse url error", func(t *testing.T) { + _, err := download(context.Background(), ":", http.DefaultClient) + if err == nil { + t.Fatal("expected error") + } + }) + t.Run("https download", func(t *testing.T) { + _, err := download(context.Background(), "https://example.com", http.DefaultClient) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("http.NewRequestWithContext error", func(t *testing.T) { + var ctx context.Context = nil + _, err := download(ctx, "http://example.com", &http.Client{}) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("client.Do error", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: errorRoundTripperMock{}, + }) + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("status code is not 2xx", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: serverErrorRoundTripperMock{}, + }) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("readAll error", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: readFailedRoundTripperMock{}, + }) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("exceed the size limit", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, + }) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("invalid crl", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: expectedRoundTripperMock{Body: []byte("invalid crl")}, + }) + if err == nil { + t.Fatal("expected error") + } + }) +} + +type errorRoundTripperMock struct{} + +func (rt errorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("error") +} + +type serverErrorRoundTripperMock struct{} + +func (rt serverErrorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + Request: req, + StatusCode: http.StatusInternalServerError, + }, nil +} + +type readFailedRoundTripperMock struct{} + +func (rt readFailedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: errorReaderMock{}, + }, nil +} + +type errorReaderMock struct{} + +func (r errorReaderMock) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("error") +} + +func (r errorReaderMock) Close() error { + return nil +} + +type expectedRoundTripperMock struct { + Body []byte +} + +func (rt expectedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + Request: req, + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(rt.Body)), + }, nil +} From f5110f92667a8cddbe1f70d11c6a1c499f66ad6c Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 9 Aug 2024 09:40:21 +0000 Subject: [PATCH 066/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/cache.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index a86ff3f2..6ff4f606 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -43,12 +43,11 @@ const ( type Cache interface { // Get retrieves the content with the given key // - // - if the key does not exist, return os.ErrNotExist + // - if the key does not exist, return NotExistError + // - if the content is expired, return ExpiredError Get(ctx context.Context, key string) (*Bundle, error) // Set stores the content with the given key - // - // - expiration is the time duration before the content is valid Set(ctx context.Context, key string, value *Bundle) error // Delete removes the content with the given key From b45fa97c656a41ecc781a87122245940cdd5aad9 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 12 Aug 2024 06:36:40 +0000 Subject: [PATCH 067/110] fix: resolve comments Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 149 +-------------- revocation/crl/cache/bundle_test.go | 213 +-------------------- revocation/crl/cache/cache.go | 14 +- revocation/crl/cache/errors.go | 29 +-- revocation/crl/cache/errors_test.go | 11 -- revocation/crl/cache/file.go | 212 +++++++++++++++++---- revocation/crl/cache/file_test.go | 244 ++++++++++++++++++++++--- revocation/crl/cache/memory.go | 52 +++--- revocation/crl/cache/memory_test.go | 28 +-- revocation/crl/fetcher/fetcher.go | 32 ++-- revocation/crl/fetcher/fetcher_test.go | 20 +- testhelper/certificatetest.go | 2 + 12 files changed, 490 insertions(+), 516 deletions(-) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index d53eaca7..41c496d1 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -14,12 +14,8 @@ package cache import ( - "archive/tar" "crypto/x509" - "encoding/json" "errors" - "fmt" - "io" "time" ) @@ -45,97 +41,22 @@ type Bundle struct { // // TODO: consider adding DeltaCRL field in the future type Metadata struct { - BaseCRL FileInfo `json:"base.crl"` + // BaseCRL stores the URL of the base CRL + BaseCRL CRLMetadata `json:"base.crl"` + + // CreateAt stores the creation time of the CRL bundle. This is different + // from the `ThisUpdate` field in the CRL. The `ThisUpdate` field in the CRL + // is the time when the CRL was generated, while the `CreateAt` field is for + // caching purpose, indicating the start of cache effective period. CreateAt time.Time `json:"createAt"` } -// FileInfo stores the URL and creation time of the file -type FileInfo struct { +// CRLMetadata stores the URL and creation time of the file +type CRLMetadata struct { URL string `json:"url"` } -// NewBundle creates a new CRL store with tarball format -func NewBundle(baseCRL *x509.RevocationList, url string) (*Bundle, error) { - return &Bundle{ - BaseCRL: baseCRL, - Metadata: Metadata{ - BaseCRL: FileInfo{ - URL: url, - }, - CreateAt: time.Now(), - }, - }, nil -} - -// ParseBundleFromTarball parses the CRL blob from a tarball -// -// The tarball should contain two files: -// - base.crl: the base CRL in DER format -// - metadata.json: the metadata of the CRL -// -// example of metadata.json: -// -// { -// "base.crl": { -// "url": "https://example.com/base.crl" -// }, -// "createAt": "2024-07-20T00:00:00Z" -// } -func ParseBundleFromTarball(data io.Reader) (*Bundle, error) { - bundle := &Bundle{} - - // parse the tarball - tar := tar.NewReader(data) - for { - header, err := tar.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, &BrokenFileError{ - Err: fmt.Errorf("failed to read tarball: %w", err), - } - } - - switch header.Name { - case PathBaseCRL: - // parse base.crl - data, err := io.ReadAll(tar) - if err != nil { - return nil, err - } - - var baseCRL *x509.RevocationList - baseCRL, err = x509.ParseRevocationList(data) - if err != nil { - return nil, &BrokenFileError{ - Err: fmt.Errorf("failed to parse base CRL from tarball: %w", err), - } - } - bundle.BaseCRL = baseCRL - case PathMetadata: - // parse metadata - var metadata Metadata - if err := json.NewDecoder(tar).Decode(&metadata); err != nil { - return nil, &BrokenFileError{ - Err: fmt.Errorf("failed to parse CRL metadata from tarball: %w", err), - } - } - bundle.Metadata = metadata - default: - return nil, &BrokenFileError{ - Err: fmt.Errorf("unexpected file in CRL tarball: %s", header.Name), - } - } - } - if err := bundle.validate(); err != nil { - return nil, err - } - - return bundle, nil -} - -func (b *Bundle) validate() error { +func (b *Bundle) Validate() error { if b.BaseCRL == nil { return errors.New("base CRL is missing") } @@ -147,53 +68,3 @@ func (b *Bundle) validate() error { } return nil } - -// SaveAsTar saves the CRL blob as a tarball, including the base CRL and -// metadata -// -// The tarball should contain two files: -// - base.crl: the base CRL in DER format -// - metadata.json: the metadata of the CRL -// -// example of metadata.json: -// -// { -// "base.crl": { -// "url": "https://example.com/base.crl" -// }, -// "createAt": "2024-06-30T00:00:00Z" -// } -func (b *Bundle) SaveAsTarball(w io.Writer) (err error) { - if err := b.validate(); err != nil { - return err - } - - tarWriter := tar.NewWriter(w) - defer tarWriter.Close() - - // Add base.crl - if err := addToTar(PathBaseCRL, b.BaseCRL.Raw, b.Metadata.CreateAt, tarWriter); err != nil { - return err - } - - // Add metadata.json - metadataBytes, err := json.Marshal(b.Metadata) - if err != nil { - return err - } - return addToTar(PathMetadata, metadataBytes, time.Now(), tarWriter) -} - -func addToTar(fileName string, data []byte, modTime time.Time, tw *tar.Writer) error { - header := &tar.Header{ - Name: fileName, - Size: int64(len(data)), - Mode: 0644, - ModTime: modTime, - } - if err := tw.WriteHeader(header); err != nil { - return err - } - _, err := tw.Write(data) - return err -} diff --git a/revocation/crl/cache/bundle_test.go b/revocation/crl/cache/bundle_test.go index 1fcb2332..8f5fd5b7 100644 --- a/revocation/crl/cache/bundle_test.go +++ b/revocation/crl/cache/bundle_test.go @@ -19,174 +19,12 @@ import ( "crypto/rand" "crypto/x509" "math/big" - "os" "testing" "time" "github.com/notaryproject/notation-core-go/testhelper" ) -func TestNewBundle(t *testing.T) { - baseCRL := &x509.RevocationList{} - url := "https://example.com/base.crl" - bundle, err := NewBundle(baseCRL, url) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if bundle.BaseCRL != baseCRL { - t.Errorf("expected BaseCRL to be %v, got %v", baseCRL, bundle.BaseCRL) - } - - if bundle.Metadata.BaseCRL.URL != url { - t.Errorf("expected URL to be %s, got %s", url, bundle.Metadata.BaseCRL.URL) - } - - if bundle.Metadata.CreateAt.IsZero() { - t.Errorf("expected CreateAt to be set, got zero value") - } -} - -func TestBundle(t *testing.T) { - const exampleURL = "https://example.com/base.crl" - var buf bytes.Buffer - - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), - }, certChain[1].Cert, certChain[1].PrivateKey) - if err != nil { - t.Fatalf("failed to create base CRL: %v", err) - } - t.Run("SaveAsTarball", func(t *testing.T) { - // Create a tarball - baseCRL, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatalf("failed to parse base CRL: %v", err) - } - bundle, err := NewBundle(baseCRL, exampleURL) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if err := bundle.SaveAsTarball(&buf); err != nil { - t.Fatalf("expected no error, got %v", err) - } - }) - - t.Run("ParseBundleFromTarball", func(t *testing.T) { - // Parse the tarball - bundle, err := ParseBundleFromTarball(&buf) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if !bytes.Equal(crlBytes, bundle.BaseCRL.Raw) { - t.Errorf("expected BaseCRL to be %v, got %v", crlBytes, bundle.BaseCRL.Raw) - } - - if bundle.Metadata.BaseCRL.URL != exampleURL { - t.Errorf("expected URL to be %s, got %s", exampleURL, bundle.Metadata.BaseCRL.URL) - } - - if bundle.Metadata.CreateAt.IsZero() { - t.Errorf("expected CreateAt to be set, got zero value") - } - }) -} - -func TestBundleParseFailed(t *testing.T) { - t.Run("IO read error", func(t *testing.T) { - _, err := ParseBundleFromTarball(&errorReader{}) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("missing baseCRL content (only has baseCRL header in tarball)", func(t *testing.T) { - var buf bytes.Buffer - header := &tar.Header{ - Name: "base.crl", - Size: 10, - Mode: 0644, - ModTime: time.Now(), - } - tw := tar.NewWriter(&buf) - if err := tw.WriteHeader(header); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Close() - - _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("broken baseCRL", func(t *testing.T) { - var buf bytes.Buffer - header := &tar.Header{ - Name: "base.crl", - Size: 10, - Mode: 0644, - ModTime: time.Now(), - } - tw := tar.NewWriter(&buf) - if err := tw.WriteHeader(header); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Write([]byte("broken crl")) - tw.Close() - - _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("malformed metadata", func(t *testing.T) { - var buf bytes.Buffer - header := &tar.Header{ - Name: "metadata.json", - Size: 10, - Mode: 0644, - ModTime: time.Now(), - } - tw := tar.NewWriter(&buf) - if err := tw.WriteHeader(header); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Write([]byte("malformed json")) - tw.Close() - - _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("unknown file in tarball", func(t *testing.T) { - var buf bytes.Buffer - header := &tar.Header{ - Name: "unknown file", - Size: 10, - Mode: 0644, - ModTime: time.Now(), - } - tw := tar.NewWriter(&buf) - if err := tw.WriteHeader(header); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Write([]byte("unknown file")) - tw.Close() - - _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) -} - func TestValidate(t *testing.T) { certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ @@ -198,7 +36,7 @@ func TestValidate(t *testing.T) { t.Run("missing BaseCRL", func(t *testing.T) { var buf bytes.Buffer - _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) if err == nil { t.Fatalf("expected error, got nil") } @@ -231,7 +69,7 @@ func TestValidate(t *testing.T) { tw.Write(metadataContent) tw.Close() - _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) if err == nil { t.Fatalf("expected error, got nil") } @@ -264,54 +102,9 @@ func TestValidate(t *testing.T) { tw.Write(metadataContent) tw.Close() - _, err := ParseBundleFromTarball(bytes.NewReader(buf.Bytes())) + _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) if err == nil { t.Fatalf("expected error, got nil") } }) } - -func TestSaveAsTarballFailed(t *testing.T) { - t.Run("validate failed", func(t *testing.T) { - bundle := &Bundle{} - if err := bundle.SaveAsTarball(&errorWriter{}); err == nil { - t.Fatalf("expected error, got nil") - } - }) - - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), - }, certChain[1].Cert, certChain[1].PrivateKey) - if err != nil { - t.Fatalf("failed to create base CRL: %v", err) - } - - t.Run("write base CRL to tarball failed", func(t *testing.T) { - crl, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatalf("failed to parse base CRL: %v", err) - } - bundle, err := NewBundle(crl, "https://example.com/base.crl") - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if err := bundle.SaveAsTarball(&errorWriter{}); err == nil { - t.Fatalf("expected error, got nil") - } - }) -} - -type errorReader struct{} - -func (r *errorReader) Read(p []byte) (n int, err error) { - return 0, os.ErrNotExist -} - -type errorWriter struct { -} - -func (w *errorWriter) Write(p []byte) (n int, err error) { - return 0, os.ErrNotExist -} diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index 6ff4f606..ba244a42 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -41,17 +41,19 @@ const ( // Cache is an interface that specifies methods used for caching type Cache interface { - // Get retrieves the content with the given key + // Get retrieves the CRL bundle with the given uri + // + // uri is the URI of the CRL // // - if the key does not exist, return NotExistError // - if the content is expired, return ExpiredError - Get(ctx context.Context, key string) (*Bundle, error) + Get(ctx context.Context, uri string) (*Bundle, error) - // Set stores the content with the given key - Set(ctx context.Context, key string, value *Bundle) error + // Set stores the CRL bundle with the given uri + Set(ctx context.Context, uri string, bundle *Bundle) error - // Delete removes the content with the given key - Delete(ctx context.Context, key string) error + // Delete removes the CRL bundle with the given uri + Delete(ctx context.Context, uri string) error // Flush removes all content Flush(ctx context.Context) error diff --git a/revocation/crl/cache/errors.go b/revocation/crl/cache/errors.go index 8c72cdbf..5923aacb 100644 --- a/revocation/crl/cache/errors.go +++ b/revocation/crl/cache/errors.go @@ -13,27 +13,7 @@ package cache -import "time" - -// NotExistError is an error type that indicates the key is not found in the -// cache. -type NotExistError struct { - Key string -} - -func (e *NotExistError) Error() string { - return "key not found: " + e.Key -} - -// ExpiredError is an error type that indicates the cache is expired. -type ExpiredError struct { - // Expires is the time when the cache expires. - Expires time.Time -} - -func (e *ExpiredError) Error() string { - return "cache expired at " + e.Expires.String() -} +import "errors" // BrokenFileError is an error type for when parsing a CRL from // a tarball @@ -47,3 +27,10 @@ type BrokenFileError struct { func (e *BrokenFileError) Error() string { return e.Err.Error() } + +var ( + // ErrCacheMiss is an error type for when a cache miss occurs + ErrCacheMiss = errors.New("cache miss") + // ErrNotFound is an error type for when a file is not found + ErrNotFound = errors.New("not found") +) diff --git a/revocation/crl/cache/errors_test.go b/revocation/crl/cache/errors_test.go index 576affe7..4d42cb0c 100644 --- a/revocation/crl/cache/errors_test.go +++ b/revocation/crl/cache/errors_test.go @@ -16,19 +16,8 @@ package cache import ( "errors" "testing" - "time" ) -func TestCacheExpiredError(t *testing.T) { - expirationTime := time.Now() - err := &ExpiredError{Expires: expirationTime} - - expectedMessage := "cache expired at " + expirationTime.String() - if err.Error() != expectedMessage { - t.Errorf("expected %q, got %q", expectedMessage, err.Error()) - } -} - func TestBrokenFileError(t *testing.T) { innerErr := errors.New("inner error") err := &BrokenFileError{Err: innerErr} diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index ca16580a..090b872e 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -14,8 +14,14 @@ package cache import ( + "archive/tar" "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" "fmt" + "io" "os" "path/filepath" "time" @@ -26,7 +32,7 @@ const ( tempFileName = "notation-*" ) -// fileCache stores in a tarball format, which contains two files: base.crl and +// FileCache stores in a tarball format, which contains two files: base.crl and // metadata.json. The base.crl file contains the base CRL in DER format, and the // metadata.json file contains the metadata of the CRL. The cache builds on top // of UNIX file system to leverage the file system concurrency control and @@ -35,91 +41,88 @@ const ( // NOTE: For Windows, the atomicity is not guaranteed. Please avoid using this // cache on Windows when the concurrent write is required. // -// fileCache doesn't handle cache cleaning but provides the Delete and Clear +// FileCache doesn't handle cache cleaning but provides the Delete and Clear // methods to remove the CRLs from the file system. -type fileCache struct { - dir string - maxAge time.Duration -} - -type FileCacheOptions struct { - Dir string +type FileCache struct { + // MaxAge is the maximum age of the CRLs cache. If the CRL is older than + // MaxAge, it will be considered as expired. MaxAge time.Duration + + root string } // NewFileCache creates a new file system store // -// - dir is the directory to store the CRLs. -// - maxAge is the maximum age of the CRLs cache. If the CRL is older than -// maxAge, it will be considered as expired. -func NewFileCache(opts *FileCacheOptions) (Cache, error) { - if err := os.MkdirAll(opts.Dir, 0700); err != nil { +// - root is the directory to store the CRLs. +func NewFileCache(root string) (*FileCache, error) { + if err := os.MkdirAll(root, 0700); err != nil { return nil, fmt.Errorf("failed to create directory: %w", err) } - cache := &fileCache{ - dir: opts.Dir, - maxAge: opts.MaxAge, - } - if cache.maxAge == 0 { - cache.maxAge = DefaultMaxAge - } - return cache, nil + return &FileCache{ + MaxAge: DefaultMaxAge, + root: root, + }, nil } // Get retrieves the CRL bundle from the file system // -// - if the key does not exist, return os.ErrNotExist -// - if the CRL is expired, return os.ErrNotExist -func (c *fileCache) Get(ctx context.Context, key string) (*Bundle, error) { - f, err := os.Open(filepath.Join(c.dir, key)) +// - if the key does not exist, return ErrNotFound +// - if the CRL is expired, return ErrCacheMiss +func (c *FileCache) Get(ctx context.Context, uri string) (bundle *Bundle, err error) { + f, err := os.Open(filepath.Join(c.root, fileName(uri))) if err != nil { if os.IsNotExist(err) { - return nil, &NotExistError{Key: key} + return nil, ErrNotFound } return nil, err } - defer f.Close() + defer func() { + if cerr := f.Close(); cerr != nil && err == nil { + err = cerr + } + }() - bundle, err := ParseBundleFromTarball(f) + bundle, err = parseBundleFromTar(f) if err != nil { return nil, err } - expires := bundle.Metadata.CreateAt.Add(c.maxAge) - if c.maxAge > 0 && time.Now().After(expires) { + expires := bundle.Metadata.CreateAt.Add(c.MaxAge) + if c.MaxAge > 0 && time.Now().After(expires) { // do not delete the file to maintain the idempotent behavior - return nil, &ExpiredError{Expires: expires} + return nil, ErrCacheMiss } return bundle, nil } // Set stores the CRL bundle in the file system -func (c *fileCache) Set(ctx context.Context, key string, bundle *Bundle) error { +func (c *FileCache) Set(ctx context.Context, uri string, bundle *Bundle) error { // save to temp file tempFile, err := os.CreateTemp("", tempFileName) if err != nil { return err } - if err := bundle.SaveAsTarball(tempFile); err != nil { + defer tempFile.Close() + + if err := saveTar(tempFile, bundle); err != nil { return err } - tempFile.Close() // rename is atomic on UNIX-like platforms - return os.Rename(tempFile.Name(), filepath.Join(c.dir, key)) + return os.Rename(tempFile.Name(), filepath.Join(c.root, fileName(uri))) } // Delete removes the CRL bundle file from file system -func (c *fileCache) Delete(ctx context.Context, key string) error { +func (c *FileCache) Delete(ctx context.Context, uri string) error { // remove is atomic on UNIX-like platforms - return os.Remove(filepath.Join(c.dir, key)) + return os.Remove(filepath.Join(c.root, fileName(uri))) } // Flush removes all CRLs from the file system -func (c *fileCache) Flush(ctx context.Context) error { - return filepath.Walk(c.dir, func(path string, info os.FileInfo, err error) error { +func (c *FileCache) Flush(ctx context.Context) error { + return filepath.Walk(c.root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -131,3 +134,132 @@ func (c *fileCache) Flush(ctx context.Context) error { return os.Remove(path) }) } + +// fileName returns the file name of the CRL bundle tarball +func fileName(url string) string { + return hashURL(url) + ".tar" +} + +// hashURL hashes the URL with SHA256 and returns the hex-encoded result +func hashURL(url string) string { + hash := sha256.Sum256([]byte(url)) + return hex.EncodeToString(hash[:]) +} + +// parseBundleFromTar parses the CRL blob from a tarball +// +// The tarball should contain two files: +// - base.crl: the base CRL in DER format +// - metadata.json: the metadata of the CRL +// +// example of metadata.json: +// +// { +// "base.crl": { +// "url": "https://example.com/base.crl" +// }, +// "createAt": "2024-07-20T00:00:00Z" +// } +func parseBundleFromTar(data io.Reader) (*Bundle, error) { + bundle := &Bundle{} + + // parse the tarball + tar := tar.NewReader(data) + for { + header, err := tar.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, &BrokenFileError{ + Err: fmt.Errorf("failed to read tarball: %w", err), + } + } + + switch header.Name { + case PathBaseCRL: + // parse base.crl + data, err := io.ReadAll(tar) + if err != nil { + return nil, err + } + + var baseCRL *x509.RevocationList + baseCRL, err = x509.ParseRevocationList(data) + if err != nil { + return nil, &BrokenFileError{ + Err: fmt.Errorf("failed to parse base CRL from tarball: %w", err), + } + } + bundle.BaseCRL = baseCRL + case PathMetadata: + // parse metadata + var metadata Metadata + if err := json.NewDecoder(tar).Decode(&metadata); err != nil { + return nil, &BrokenFileError{ + Err: fmt.Errorf("failed to parse CRL metadata from tarball: %w", err), + } + } + bundle.Metadata = metadata + } + } + if err := bundle.Validate(); err != nil { + return nil, err + } + + return bundle, nil +} + +// SaveAsTar saves the CRL blob as a tarball, including the base CRL and +// metadata +// +// The tarball should contain two files: +// - base.crl: the base CRL in DER format +// - metadata.json: the metadata of the CRL +// +// example of metadata.json: +// +// { +// "base.crl": { +// "url": "https://example.com/base.crl" +// }, +// "createAt": "2024-06-30T00:00:00Z" +// } +func saveTar(w io.Writer, bundle *Bundle) (err error) { + if err := bundle.Validate(); err != nil { + return err + } + + tarWriter := tar.NewWriter(w) + defer func() { + if cerr := tarWriter.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + // Add base.crl + if err := addToTar(PathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CreateAt, tarWriter); err != nil { + return err + } + + // Add metadata.json + metadataBytes, err := json.Marshal(bundle.Metadata) + if err != nil { + return err + } + return addToTar(PathMetadata, metadataBytes, time.Now(), tarWriter) +} + +func addToTar(fileName string, data []byte, modTime time.Time, tw *tar.Writer) error { + header := &tar.Header{ + Name: fileName, + Size: int64(len(data)), + Mode: 0644, + ModTime: modTime, + } + if err := tw.WriteHeader(header); err != nil { + return err + } + _, err := tw.Write(data) + return err +} diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index 8bd826cf..2eee68c3 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -14,6 +14,8 @@ package cache import ( + "archive/tar" + "bytes" "context" "crypto/rand" "crypto/x509" @@ -41,23 +43,22 @@ func TestFileCache(t *testing.T) { } ctx := context.Background() - dir := t.TempDir() - opts := &FileCacheOptions{Dir: dir, MaxAge: 5 * time.Minute} - cache, err := NewFileCache(opts) + root := t.TempDir() + cache, err := NewFileCache(root) t.Run("NewFileCache", func(t *testing.T) { if err != nil { t.Fatalf("expected no error, got %v", err) } - if cache.(*fileCache).dir != opts.Dir { - t.Fatalf("expected dir %v, got %v", opts.Dir, cache.(*fileCache).dir) + if cache.root != root { + t.Fatalf("expected dir %v, got %v", root, cache.root) } - if cache.(*fileCache).maxAge != opts.MaxAge { - t.Fatalf("expected maxAge %v, got %v", opts.MaxAge, cache.(*fileCache).maxAge) + if cache.MaxAge != DefaultMaxAge { + t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.MaxAge) } }) key := "testKey" - bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: FileInfo{URL: "http://crl"}, CreateAt: time.Now()}} + bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreateAt: time.Now()}} t.Run("SetAndGet", func(t *testing.T) { if err := cache.Set(ctx, key, bundle); err != nil { t.Fatalf("expected no error, got %v", err) @@ -72,13 +73,13 @@ func TestFileCache(t *testing.T) { }) t.Run("GetWithExpiredBundle", func(t *testing.T) { - expiredBundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: FileInfo{URL: "http://crl"}, CreateAt: time.Now().Add(-10 * time.Minute)}} + expiredBundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreateAt: time.Now().Add(-DefaultMaxAge - 1*time.Second)}} if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { t.Fatalf("expected no error, got %v", err) } _, err = cache.Get(ctx, "expiredKey") - if _, ok := err.(*ExpiredError); !ok { - t.Fatalf("expected CacheExpiredError, got %v", err) + if !errors.Is(err, ErrCacheMiss) { + t.Fatalf("expected ErrCacheMiss, got %v", err) } }) @@ -88,8 +89,7 @@ func TestFileCache(t *testing.T) { t.Fatalf("expected no error, got %v", err) } _, err = cache.Get(ctx, key) - var notExistError *NotExistError - if !errors.As(err, ¬ExistError) { + if !errors.Is(err, ErrNotFound) { t.Fatalf("expected error, got nil") } }) @@ -104,16 +104,19 @@ func TestFileCache(t *testing.T) { if err := cache.Flush(ctx); err != nil { t.Fatalf("expected no error, got %v", err) } - var notExistError *NotExistError _, err = cache.Get(ctx, "key1") - if !errors.As(err, ¬ExistError) { + if !errors.Is(err, ErrNotFound) { t.Fatalf("expected error, got nil") } _, err = cache.Get(ctx, "key2") - if !errors.As(err, ¬ExistError) { + if !errors.Is(err, ErrNotFound) { t.Fatalf("expected error, got nil") } }) + + t.Run("Cache interface", func(t *testing.T) { + var _ Cache = cache + }) } func TestNewFileCache(t *testing.T) { @@ -122,7 +125,8 @@ func TestNewFileCache(t *testing.T) { if err := os.Chmod(tempDir, 0); err != nil { t.Fatalf("failed to change permission: %v", err) } - _, err := NewFileCache(&FileCacheOptions{Dir: filepath.Join(tempDir, "test")}) + root := filepath.Join(tempDir, "test") + _, err := NewFileCache(root) if err == nil { t.Fatalf("expected error, got nil") } @@ -133,12 +137,12 @@ func TestNewFileCache(t *testing.T) { }) t.Run("no maxAge", func(t *testing.T) { - cache, err := NewFileCache(&FileCacheOptions{Dir: tempDir}) + cache, err := NewFileCache(t.TempDir()) if err != nil { t.Fatalf("expected no error, got %v", err) } - if cache.(*fileCache).maxAge != DefaultMaxAge { - t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.(*fileCache).maxAge) + if cache.MaxAge != DefaultMaxAge { + t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.MaxAge) } }) } @@ -151,7 +155,7 @@ func TestGetFailed(t *testing.T) { t.Fatalf("failed to write file: %v", err) } - cache, err := NewFileCache(&FileCacheOptions{Dir: tempDir}) + cache, err := NewFileCache(tempDir) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -180,7 +184,7 @@ func TestGetFailed(t *testing.T) { func TestSetFailed(t *testing.T) { tempDir := t.TempDir() - cache, err := NewFileCache(&FileCacheOptions{Dir: tempDir}) + cache, err := NewFileCache(tempDir) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -195,7 +199,7 @@ func TestSetFailed(t *testing.T) { func TestFlushFailed(t *testing.T) { tempDir := t.TempDir() - cache, err := NewFileCache(&FileCacheOptions{Dir: tempDir}) + cache, err := NewFileCache(tempDir) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -213,3 +217,197 @@ func TestFlushFailed(t *testing.T) { } }) } + +func TestParseAndSave(t *testing.T) { + const exampleURL = "https://example.com/base.crl" + var buf bytes.Buffer + + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + t.Run("SaveAsTarball", func(t *testing.T) { + // Create a tarball + baseCRL, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } + bundle := &Bundle{ + BaseCRL: baseCRL, + Metadata: Metadata{ + BaseCRL: CRLMetadata{ + URL: exampleURL, + }, + CreateAt: time.Now(), + }, + } + + if err := saveTar(&buf, bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + }) + + t.Run("ParseBundleFromTarball", func(t *testing.T) { + // Parse the tarball + bundle, err := parseBundleFromTar(&buf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if !bytes.Equal(crlBytes, bundle.BaseCRL.Raw) { + t.Errorf("expected BaseCRL to be %v, got %v", crlBytes, bundle.BaseCRL.Raw) + } + + if bundle.Metadata.BaseCRL.URL != exampleURL { + t.Errorf("expected URL to be %s, got %s", exampleURL, bundle.Metadata.BaseCRL.URL) + } + + if bundle.Metadata.CreateAt.IsZero() { + t.Errorf("expected CreateAt to be set, got zero value") + } + }) +} + +func TestBundleParseFailed(t *testing.T) { + t.Run("IO read error", func(t *testing.T) { + _, err := parseBundleFromTar(&errorReader{}) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("missing baseCRL content (only has baseCRL header in tarball)", func(t *testing.T) { + var buf bytes.Buffer + header := &tar.Header{ + Name: "base.crl", + Size: 10, + Mode: 0644, + ModTime: time.Now(), + } + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Close() + + _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("broken baseCRL", func(t *testing.T) { + var buf bytes.Buffer + header := &tar.Header{ + Name: "base.crl", + Size: 10, + Mode: 0644, + ModTime: time.Now(), + } + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write([]byte("broken crl")) + tw.Close() + + _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("malformed metadata", func(t *testing.T) { + var buf bytes.Buffer + header := &tar.Header{ + Name: "metadata.json", + Size: 10, + Mode: 0644, + ModTime: time.Now(), + } + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write([]byte("malformed json")) + tw.Close() + + _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("unknown file in tarball", func(t *testing.T) { + var buf bytes.Buffer + header := &tar.Header{ + Name: "unknown file", + Size: 10, + Mode: 0644, + ModTime: time.Now(), + } + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write header: %v", err) + } + tw.Write([]byte("unknown file")) + tw.Close() + + _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) +} + +func TestSaveTarFailed(t *testing.T) { + t.Run("validate failed", func(t *testing.T) { + bundle := &Bundle{} + if err := saveTar(&errorWriter{}, bundle); err == nil { + t.Fatalf("expected error, got nil") + } + }) + + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + + t.Run("write base CRL to tarball failed", func(t *testing.T) { + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } + bundle := &Bundle{ + BaseCRL: crl, + Metadata: Metadata{ + BaseCRL: CRLMetadata{ + URL: "https://example.com/base.crl", + }, + CreateAt: time.Now(), + }, + } + if err := saveTar(&errorWriter{}, bundle); err == nil { + t.Fatalf("expected error, got nil") + } + }) +} + +type errorReader struct{} + +func (r *errorReader) Read(p []byte) (n int, err error) { + return 0, os.ErrNotExist +} + +type errorWriter struct { +} + +func (w *errorWriter) Write(p []byte) (n int, err error) { + return 0, os.ErrNotExist +} diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index f3a73da2..ddcf22e9 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -20,73 +20,65 @@ import ( "time" ) -// memoryCache is an in-memory cache that stores CRL bundles. +// MemoryCache is an in-memory cache that stores CRL bundles. // // The cache is built on top of the sync.Map to leverage the concurrency control // and atomicity of the map, so it is suitable for writing once and reading many // times. The CRL is stored in memory as a Bundle type. // -// memoryCache doesn't handle cache cleaning but provides the Delete and Clear +// MemoryCache doesn't handle cache cleaning but provides the Delete and Clear // methods to remove the CRLs from the memory. -type memoryCache struct { - store sync.Map - maxAge time.Duration -} - -type MemoryCacheOptions struct { +type MemoryCache struct { + // MaxAge is the maximum age of the CRLs cache. If the CRL is older than + // MaxAge, it will be considered as expired. MaxAge time.Duration + + store sync.Map } // NewMemoryCache creates a new memory store. // // - maxAge is the maximum age of the CRLs cache. If the CRL is older than // maxAge, it will be considered as expired. -func NewMemoryCache(opts MemoryCacheOptions) (Cache, error) { - c := &memoryCache{ - maxAge: opts.MaxAge, - } - - if c.maxAge == 0 { - c.maxAge = DefaultMaxAge - } - - return c, nil +func NewMemoryCache() (*MemoryCache, error) { + return &MemoryCache{ + MaxAge: DefaultMaxAge, + }, nil } // Get retrieves the CRL from the memory store. -func (c *memoryCache) Get(ctx context.Context, key string) (*Bundle, error) { - value, ok := c.store.Load(key) +func (c *MemoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { + value, ok := c.store.Load(uri) if !ok { - return nil, &NotExistError{Key: key} + return nil, ErrNotFound } - bundle, ok := value.(*Bundle) if !ok { return nil, fmt.Errorf("invalid type: %T", value) } - expires := bundle.Metadata.CreateAt.Add(c.maxAge) - if c.maxAge > 0 && time.Now().After(expires) { - return nil, &ExpiredError{Expires: expires} + expires := bundle.Metadata.CreateAt.Add(c.MaxAge) + if c.MaxAge > 0 && time.Now().After(expires) { + return nil, ErrCacheMiss } return bundle, nil } // Set stores the CRL in the memory store. -func (c *memoryCache) Set(ctx context.Context, key string, bundle *Bundle) error { - c.store.Store(key, bundle) +func (c *MemoryCache) Set(ctx context.Context, uri string, bundle *Bundle) error { + c.store.Store(uri, bundle) return nil } // Delete removes the CRL from the memory store. -func (c *memoryCache) Delete(ctx context.Context, key string) error { - c.store.Delete(key) +func (c *MemoryCache) Delete(ctx context.Context, uri string) error { + c.store.Delete(uri) return nil } // Flush removes all CRLs from the memory store. -func (c *memoryCache) Flush(ctx context.Context) error { +func (c *MemoryCache) Flush(ctx context.Context) error { c.store.Range(func(key, value interface{}) bool { c.store.Delete(key) return true diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index 3a60ed7a..4e4a6391 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -15,6 +15,7 @@ package cache import ( "context" + "errors" "testing" "time" ) @@ -23,13 +24,12 @@ func TestMemoryCache(t *testing.T) { ctx := context.Background() // Test NewMemoryCache - opts := MemoryCacheOptions{MaxAge: 5 * time.Minute} - cache, err := NewMemoryCache(opts) + cache, err := NewMemoryCache() if err != nil { t.Fatalf("expected no error, got %v", err) } - if cache.(*memoryCache).maxAge != opts.MaxAge { - t.Fatalf("expected maxAge %v, got %v", opts.MaxAge, cache.(*memoryCache).maxAge) + if cache.MaxAge != DefaultMaxAge { + t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.MaxAge) } bundle := &Bundle{Metadata: Metadata{CreateAt: time.Now()}} @@ -48,13 +48,13 @@ func TestMemoryCache(t *testing.T) { }) t.Run("GetWithExpiredBundle", func(t *testing.T) { - expiredBundle := &Bundle{Metadata: Metadata{CreateAt: time.Now().Add(-10 * time.Minute)}} + expiredBundle := &Bundle{Metadata: Metadata{CreateAt: time.Now().Add(-DefaultMaxAge - 1*time.Second)}} if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { t.Fatalf("expected no error, got %v", err) } _, err = cache.Get(ctx, "expiredKey") - if _, ok := err.(*ExpiredError); !ok { - t.Fatalf("expected CacheExpiredError, got %v", err) + if !errors.Is(err, ErrCacheMiss) { + t.Fatalf("expected ErrCacheMiss, got %v", err) } }) @@ -63,7 +63,7 @@ func TestMemoryCache(t *testing.T) { t.Fatalf("expected no error, got %v", err) } _, err = cache.Get(ctx, key) - if _, ok := err.(*NotExistError); !ok { + if !errors.Is(err, ErrNotFound) { t.Fatalf("expected error, got nil") } }) @@ -79,25 +79,29 @@ func TestMemoryCache(t *testing.T) { t.Fatalf("expected no error, got %v", err) } _, err = cache.Get(ctx, "key1") - if _, ok := err.(*NotExistError); !ok { + if !errors.Is(err, ErrNotFound) { t.Fatalf("expected error, got nil") } _, err = cache.Get(ctx, "key2") - if _, ok := err.(*NotExistError); !ok { + if !errors.Is(err, ErrNotFound) { t.Fatalf("expected error, got nil") } }) + + t.Run("Cache interface", func(t *testing.T) { + var _ Cache = cache + }) } func TestMemoryCacheFailed(t *testing.T) { ctx := context.Background() // Test Get with invalid type - cache, err := NewMemoryCache(MemoryCacheOptions{}) + cache, err := NewMemoryCache() if err != nil { t.Fatalf("expected no error, got %v", err) } - cache.(*memoryCache).store.Store("invalidKey", "invalidValue") + cache.store.Store("invalidKey", "invalidValue") _, err = cache.Get(ctx, "invalidKey") if err == nil { t.Fatalf("expected error, got nil") diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher/fetcher.go index 798e63e5..c901a241 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -17,14 +17,13 @@ package fetcher import ( "context" - "crypto/sha256" "crypto/x509" - "encoding/hex" "errors" "fmt" "io" "net/http" "net/url" + "time" "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) @@ -72,12 +71,12 @@ func (f *cachedFetcher) Fetch(ctx context.Context, crlURL string) (bundle *cache } // try to get from cache - bundle, err = f.cacheClient.Get(ctx, tarStoreName(crlURL)) + bundle, err = f.cacheClient.Get(ctx, crlURL) if err != nil { - var notExistError *cache.NotExistError var cacheBrokenError *cache.BrokenFileError - if errors.As(err, ¬ExistError) || errors.As(err, &cacheBrokenError) { - // download if not exist or broken + if errors.Is(err, cache.ErrNotFound) || + errors.Is(err, cache.ErrCacheMiss) || + errors.As(err, &cacheBrokenError) { bundle, err = f.Download(ctx, crlURL) if err != nil { return nil, false, err @@ -100,7 +99,7 @@ func (f *cachedFetcher) Download(ctx context.Context, crlURL string) (bundle *ca } // save to cache - if err := f.cacheClient.Set(ctx, tarStoreName(crlURL), bundle); err != nil { + if err := f.cacheClient.Set(ctx, crlURL, bundle); err != nil { return nil, fmt.Errorf("failed to save to cache: %w", err) } @@ -144,15 +143,14 @@ func download(ctx context.Context, crlURL string, client *http.Client) (bundle * if err != nil { return nil, fmt.Errorf("failed to parse CRL: %w", err) } - return cache.NewBundle(crl, crlURL) -} - -func tarStoreName(url string) string { - return hashURL(url) + ".tar" -} -// hashURL hashes the URL with SHA256 and returns the hex-encoded result -func hashURL(url string) string { - hash := sha256.Sum256([]byte(url)) - return hex.EncodeToString(hash[:]) + return &cache.Bundle{ + BaseCRL: crl, + Metadata: cache.Metadata{ + BaseCRL: cache.CRLMetadata{ + URL: crlURL, + }, + CreateAt: time.Now(), + }, + }, nil } diff --git a/revocation/crl/fetcher/fetcher_test.go b/revocation/crl/fetcher/fetcher_test.go index 0b6677a5..6b949bdb 100644 --- a/revocation/crl/fetcher/fetcher_test.go +++ b/revocation/crl/fetcher/fetcher_test.go @@ -23,13 +23,14 @@ import ( "math/big" "net/http" "testing" + "time" "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/testhelper" ) func TestNewCachedFetcher(t *testing.T) { - c, err := cache.NewMemoryCache(cache.MemoryCacheOptions{}) + c, err := cache.NewMemoryCache() if err != nil { t.Errorf("NewMemoryCache() error = %v, want nil", err) } @@ -50,7 +51,7 @@ func TestNewCachedFetcher(t *testing.T) { func TestFetch(t *testing.T) { // prepare cache - c, err := cache.NewMemoryCache(cache.MemoryCacheOptions{}) + c, err := cache.NewMemoryCache() if err != nil { t.Errorf("NewMemoryCache() error = %v, want nil", err) } @@ -70,11 +71,16 @@ func TestFetch(t *testing.T) { const exampleURL = "http://example.com" const uncachedURL = "http://uncached.com" - bundle, err := cache.NewBundle(baseCRL, exampleURL) - if err != nil { - t.Errorf("NewBundle() error = %v, want nil", err) + bundle := &cache.Bundle{ + BaseCRL: baseCRL, + Metadata: cache.Metadata{ + BaseCRL: cache.CRLMetadata{ + URL: exampleURL, + }, + CreateAt: time.Now(), + }, } - if err := c.Set(context.Background(), tarStoreName(exampleURL), bundle); err != nil { + if err := c.Set(context.Background(), exampleURL, bundle); err != nil { t.Errorf("Cache.Set() error = %v, want nil", err) } @@ -138,7 +144,7 @@ func TestFetch(t *testing.T) { } // delete cache - if err := c.Delete(context.Background(), tarStoreName(uncachedURL)); err != nil { + if err := c.Delete(context.Background(), uncachedURL); err != nil { t.Errorf("Cache.Delete() error = %v, want nil", err) } }) diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index e1e3d7e4..a06e5035 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -75,6 +75,8 @@ func GetRevokableRSALeafCertificate() RSACertTuple { return revokableRSALeaf } +// GetRevokableRSAChainWithRevocations returns a certificate chain with OCSP +// and CRL enabled for revocation checks. func GetRevokableRSAChainWithRevocations(size int, enabledOCSP, enabledCRL bool) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) From 78839caac12c9352f4d64736e027c2f2857e9e9e Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 12 Aug 2024 07:37:42 +0000 Subject: [PATCH 068/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/file.go | 17 ++++++++++++----- revocation/crl/cache/file_test.go | 21 --------------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 090b872e..2a123063 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -34,12 +34,19 @@ const ( // FileCache stores in a tarball format, which contains two files: base.crl and // metadata.json. The base.crl file contains the base CRL in DER format, and the -// metadata.json file contains the metadata of the CRL. The cache builds on top -// of UNIX file system to leverage the file system concurrency control and -// atomicity. +// metadata.json file contains the metadata of the CRL. // -// NOTE: For Windows, the atomicity is not guaranteed. Please avoid using this -// cache on Windows when the concurrent write is required. +// The cache builds on top of the UNIX file system to leverage the file system's +// atomic operations. The `rename` and `remove` operations will unlink the old +// file but keep the inode and file descriptor for existing processes to access +// the file. The old inode will be dereferenced when all processes close the old +// file descriptor. Additionally, the operations are proven to be atomic on +// UNIX-like platforms, so there is no need to handle file locking. +// +// NOTE: For Windows, the `open`, `rename` and `remove` operations need file +// locking to ensure atomicity. The current implementation does not handle +// file locking, so the concurrent write from multiple processes may be failed. +// Please do not use this cache in a multi-process environment on Windows. // // FileCache doesn't handle cache cleaning but provides the Delete and Clear // methods to remove the CRLs from the file system. diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index 2eee68c3..032781df 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -340,27 +340,6 @@ func TestBundleParseFailed(t *testing.T) { t.Fatalf("expected error, got nil") } }) - - t.Run("unknown file in tarball", func(t *testing.T) { - var buf bytes.Buffer - header := &tar.Header{ - Name: "unknown file", - Size: 10, - Mode: 0644, - ModTime: time.Now(), - } - tw := tar.NewWriter(&buf) - if err := tw.WriteHeader(header); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Write([]byte("unknown file")) - tw.Close() - - _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) } func TestSaveTarFailed(t *testing.T) { From 089124cd4d23a0ed8b1cfdb2940e994ffcd0cdd4 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Mon, 12 Aug 2024 07:47:41 +0000 Subject: [PATCH 069/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/cache.go | 4 ++-- revocation/crl/cache/memory.go | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index ba244a42..29be8729 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -45,8 +45,8 @@ type Cache interface { // // uri is the URI of the CRL // - // - if the key does not exist, return NotExistError - // - if the content is expired, return ExpiredError + // - if the key does not exist, return ErrNotFound + // - if the content is expired, return ErrCacheMiss Get(ctx context.Context, uri string) (*Bundle, error) // Set stores the CRL bundle with the given uri diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index ddcf22e9..d859bf4b 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -47,6 +47,9 @@ func NewMemoryCache() (*MemoryCache, error) { } // Get retrieves the CRL from the memory store. +// +// - if the key does not exist, return ErrNotFound +// - if the CRL is expired, return ErrCacheMiss func (c *MemoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { value, ok := c.store.Load(uri) if !ok { From 8a30ada55bacd32ce397cad0c86945ed7c189f32 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 14 Aug 2024 02:31:29 +0000 Subject: [PATCH 070/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 22 ++++----- revocation/crl/cache/cache.go | 9 +--- revocation/crl/cache/errors.go | 2 - revocation/crl/cache/file.go | 31 ++---------- revocation/crl/cache/file_test.go | 59 ----------------------- revocation/crl/cache/memory.go | 21 ++------- revocation/crl/cache/memory_test.go | 65 +++++++++++++------------- revocation/crl/fetcher/fetcher.go | 3 +- revocation/crl/fetcher/fetcher_test.go | 11 ++--- 9 files changed, 61 insertions(+), 162 deletions(-) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index 41c496d1..a2135cb7 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -27,14 +27,9 @@ const ( PathMetadata = "metadata.json" ) -// Bundle is in memory representation of the Bundle tarball, including base CRL -// file and metadata file, which may be cached in the file system or other -// storage -// -// TODO: consider adding DeltaCRL field in the future -type Bundle struct { - BaseCRL *x509.RevocationList - Metadata Metadata +// CRLMetadata stores the URL and creation time of the file +type CRLMetadata struct { + URL string `json:"url"` } // Metadata stores the metadata infomation of the CRL @@ -51,9 +46,14 @@ type Metadata struct { CreateAt time.Time `json:"createAt"` } -// CRLMetadata stores the URL and creation time of the file -type CRLMetadata struct { - URL string `json:"url"` +// Bundle is in memory representation of the Bundle tarball, including base CRL +// file and metadata file, which may be cached in the file system or other +// storage +// +// TODO: consider adding DeltaCRL field in the future +type Bundle struct { + BaseCRL *x509.RevocationList + Metadata Metadata } func (b *Bundle) Validate() error { diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index 29be8729..3497f4b6 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -45,16 +45,9 @@ type Cache interface { // // uri is the URI of the CRL // - // - if the key does not exist, return ErrNotFound - // - if the content is expired, return ErrCacheMiss + // if the key does not exist or the content is expired, return ErrCacheMiss. Get(ctx context.Context, uri string) (*Bundle, error) // Set stores the CRL bundle with the given uri Set(ctx context.Context, uri string, bundle *Bundle) error - - // Delete removes the CRL bundle with the given uri - Delete(ctx context.Context, uri string) error - - // Flush removes all content - Flush(ctx context.Context) error } diff --git a/revocation/crl/cache/errors.go b/revocation/crl/cache/errors.go index 5923aacb..adfb8a13 100644 --- a/revocation/crl/cache/errors.go +++ b/revocation/crl/cache/errors.go @@ -31,6 +31,4 @@ func (e *BrokenFileError) Error() string { var ( // ErrCacheMiss is an error type for when a cache miss occurs ErrCacheMiss = errors.New("cache miss") - // ErrNotFound is an error type for when a file is not found - ErrNotFound = errors.New("not found") ) diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 2a123063..b5eaac49 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -80,7 +80,7 @@ func (c *FileCache) Get(ctx context.Context, uri string) (bundle *Bundle, err er f, err := os.Open(filepath.Join(c.root, fileName(uri))) if err != nil { if os.IsNotExist(err) { - return nil, ErrNotFound + return nil, ErrCacheMiss } return nil, err } @@ -106,6 +106,10 @@ func (c *FileCache) Get(ctx context.Context, uri string) (bundle *Bundle, err er // Set stores the CRL bundle in the file system func (c *FileCache) Set(ctx context.Context, uri string, bundle *Bundle) error { + if err := bundle.Validate(); err != nil { + return err + } + // save to temp file tempFile, err := os.CreateTemp("", tempFileName) if err != nil { @@ -121,27 +125,6 @@ func (c *FileCache) Set(ctx context.Context, uri string, bundle *Bundle) error { return os.Rename(tempFile.Name(), filepath.Join(c.root, fileName(uri))) } -// Delete removes the CRL bundle file from file system -func (c *FileCache) Delete(ctx context.Context, uri string) error { - // remove is atomic on UNIX-like platforms - return os.Remove(filepath.Join(c.root, fileName(uri))) -} - -// Flush removes all CRLs from the file system -func (c *FileCache) Flush(ctx context.Context) error { - return filepath.Walk(c.root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - - // remove is atomic on UNIX-like platforms - return os.Remove(path) - }) -} - // fileName returns the file name of the CRL bundle tarball func fileName(url string) string { return hashURL(url) + ".tar" @@ -233,10 +216,6 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { // "createAt": "2024-06-30T00:00:00Z" // } func saveTar(w io.Writer, bundle *Bundle) (err error) { - if err := bundle.Validate(); err != nil { - return err - } - tarWriter := tar.NewWriter(w) defer func() { if cerr := tarWriter.Close(); cerr != nil && err == nil { diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index 032781df..f4aa839e 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -83,37 +83,6 @@ func TestFileCache(t *testing.T) { } }) - t.Run("Delete", func(t *testing.T) { - // Test Delete - if err := cache.Delete(ctx, key); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = cache.Get(ctx, key) - if !errors.Is(err, ErrNotFound) { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("Flush", func(t *testing.T) { - if err := cache.Set(ctx, "key1", bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - if err := cache.Set(ctx, "key2", bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - if err := cache.Flush(ctx); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = cache.Get(ctx, "key1") - if !errors.Is(err, ErrNotFound) { - t.Fatalf("expected error, got nil") - } - _, err = cache.Get(ctx, "key2") - if !errors.Is(err, ErrNotFound) { - t.Fatalf("expected error, got nil") - } - }) - t.Run("Cache interface", func(t *testing.T) { var _ Cache = cache }) @@ -197,27 +166,6 @@ func TestSetFailed(t *testing.T) { }) } -func TestFlushFailed(t *testing.T) { - tempDir := t.TempDir() - cache, err := NewFileCache(tempDir) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - t.Run("failed to remove files", func(t *testing.T) { - if err := os.Chmod(tempDir, 0); err != nil { - t.Fatalf("failed to change permission: %v", err) - } - if err := cache.Flush(context.Background()); err == nil { - t.Fatalf("expected error, got nil") - } - // restore permission - if err := os.Chmod(tempDir, 0755); err != nil { - t.Fatalf("failed to change permission: %v", err) - } - }) -} - func TestParseAndSave(t *testing.T) { const exampleURL = "https://example.com/base.crl" var buf bytes.Buffer @@ -343,13 +291,6 @@ func TestBundleParseFailed(t *testing.T) { } func TestSaveTarFailed(t *testing.T) { - t.Run("validate failed", func(t *testing.T) { - bundle := &Bundle{} - if err := saveTar(&errorWriter{}, bundle); err == nil { - t.Fatalf("expected error, got nil") - } - }) - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ Number: big.NewInt(1), diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index d859bf4b..e657713a 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -53,7 +53,7 @@ func NewMemoryCache() (*MemoryCache, error) { func (c *MemoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { value, ok := c.store.Load(uri) if !ok { - return nil, ErrNotFound + return nil, ErrCacheMiss } bundle, ok := value.(*Bundle) if !ok { @@ -70,21 +70,10 @@ func (c *MemoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { // Set stores the CRL in the memory store. func (c *MemoryCache) Set(ctx context.Context, uri string, bundle *Bundle) error { - c.store.Store(uri, bundle) - return nil -} - -// Delete removes the CRL from the memory store. -func (c *MemoryCache) Delete(ctx context.Context, uri string) error { - c.store.Delete(uri) - return nil -} + if err := bundle.Validate(); err != nil { + return err + } -// Flush removes all CRLs from the memory store. -func (c *MemoryCache) Flush(ctx context.Context) error { - c.store.Range(func(key, value interface{}) bool { - c.store.Delete(key) - return true - }) + c.store.Store(uri, bundle) return nil } diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index 4e4a6391..29bac3fa 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -15,14 +15,31 @@ package cache import ( "context" + "crypto/rand" + "crypto/x509" "errors" + "math/big" "testing" "time" + + "github.com/notaryproject/notation-core-go/testhelper" ) func TestMemoryCache(t *testing.T) { ctx := context.Background() + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + baseCRL, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } + // Test NewMemoryCache cache, err := NewMemoryCache() if err != nil { @@ -32,7 +49,14 @@ func TestMemoryCache(t *testing.T) { t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.MaxAge) } - bundle := &Bundle{Metadata: Metadata{CreateAt: time.Now()}} + bundle := &Bundle{ + BaseCRL: baseCRL, + Metadata: Metadata{ + CreateAt: time.Now(), + BaseCRL: CRLMetadata{ + URL: "http://crl", + }, + }} key := "testKey" t.Run("SetAndGet", func(t *testing.T) { if err := cache.Set(ctx, key, bundle); err != nil { @@ -48,7 +72,14 @@ func TestMemoryCache(t *testing.T) { }) t.Run("GetWithExpiredBundle", func(t *testing.T) { - expiredBundle := &Bundle{Metadata: Metadata{CreateAt: time.Now().Add(-DefaultMaxAge - 1*time.Second)}} + expiredBundle := &Bundle{ + BaseCRL: baseCRL, + Metadata: Metadata{ + CreateAt: time.Now().Add(-DefaultMaxAge - 1*time.Second), + BaseCRL: CRLMetadata{ + URL: "http://crl", + }, + }} if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { t.Fatalf("expected no error, got %v", err) } @@ -58,36 +89,6 @@ func TestMemoryCache(t *testing.T) { } }) - t.Run("Delete", func(t *testing.T) { - if err := cache.Delete(ctx, key); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = cache.Get(ctx, key) - if !errors.Is(err, ErrNotFound) { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("Flush", func(t *testing.T) { - if err := cache.Set(ctx, "key1", bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - if err := cache.Set(ctx, "key2", bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - if err := cache.Flush(ctx); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = cache.Get(ctx, "key1") - if !errors.Is(err, ErrNotFound) { - t.Fatalf("expected error, got nil") - } - _, err = cache.Get(ctx, "key2") - if !errors.Is(err, ErrNotFound) { - t.Fatalf("expected error, got nil") - } - }) - t.Run("Cache interface", func(t *testing.T) { var _ Cache = cache }) diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher/fetcher.go index c901a241..ebfed139 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -74,8 +74,7 @@ func (f *cachedFetcher) Fetch(ctx context.Context, crlURL string) (bundle *cache bundle, err = f.cacheClient.Get(ctx, crlURL) if err != nil { var cacheBrokenError *cache.BrokenFileError - if errors.Is(err, cache.ErrNotFound) || - errors.Is(err, cache.ErrCacheMiss) || + if errors.Is(err, cache.ErrCacheMiss) || errors.As(err, &cacheBrokenError) { bundle, err = f.Download(ctx, crlURL) if err != nil { diff --git a/revocation/crl/fetcher/fetcher_test.go b/revocation/crl/fetcher/fetcher_test.go index 6b949bdb..142d9f11 100644 --- a/revocation/crl/fetcher/fetcher_test.go +++ b/revocation/crl/fetcher/fetcher_test.go @@ -142,18 +142,17 @@ func TestFetch(t *testing.T) { if !bytes.Equal(fetchedBundle.BaseCRL.Raw, baseCRL.Raw) { t.Errorf("Fetcher.Fetch() fetchedBundle.BaseCRL.Raw = %v, want %v", fetchedBundle.BaseCRL.Raw, baseCRL.Raw) } - - // delete cache - if err := c.Delete(context.Background(), uncachedURL); err != nil { - t.Errorf("Cache.Delete() error = %v, want nil", err) - } }) t.Run("cache miss and download failed error", func(t *testing.T) { httpClient := &http.Client{ Transport: errorRoundTripperMock{}, } - f, err := NewCachedFetcher(httpClient, c) + newCache, err := cache.NewMemoryCache() + if err != nil { + t.Errorf("NewMemoryCache() error = %v, want nil", err) + } + f, err := NewCachedFetcher(httpClient, newCache) if err != nil { t.Errorf("NewCachedFetcher() error = %v, want nil", err) } From 5dcd1f12923e06fdfe16b8290a26205c84994988 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 14 Aug 2024 02:59:57 +0000 Subject: [PATCH 071/110] test: add unit test Signed-off-by: Junjie Gao --- revocation/crl/cache/file_test.go | 18 +++++++++++ revocation/crl/cache/memory_test.go | 46 +++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index f4aa839e..0353e063 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -23,6 +23,7 @@ import ( "math/big" "os" "path/filepath" + "strings" "testing" "time" @@ -149,6 +150,23 @@ func TestGetFailed(t *testing.T) { t.Fatalf("failed to change permission: %v", err) } }) + + t.Run("invalid bundle file", func(t *testing.T) { + bundle := &Bundle{ + BaseCRL: &x509.RevocationList{Raw: []byte("invalid crl")}, + Metadata: Metadata{CreateAt: time.Now()}, + } + if err := saveTar(&bytes.Buffer{}, bundle); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if err := os.WriteFile(filepath.Join(tempDir, fileName("invalid")), []byte("invalid tarball"), 0644); err != nil { + t.Fatalf("failed to write file: %v", err) + } + _, err = cache.Get(context.Background(), "invalid") + if !strings.Contains(err.Error(), "failed to read tarball") { + t.Fatalf("expected error, got %v", err) + } + }) } func TestSetFailed(t *testing.T) { diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index 29bac3fa..fe6d33cc 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -89,6 +89,13 @@ func TestMemoryCache(t *testing.T) { } }) + t.Run("Key doesn't exist", func(t *testing.T) { + _, err := cache.Get(ctx, "nonExistentKey") + if !errors.Is(err, ErrCacheMiss) { + t.Fatalf("expected ErrCacheMiss, got %v", err) + } + }) + t.Run("Cache interface", func(t *testing.T) { var _ Cache = cache }) @@ -98,13 +105,34 @@ func TestMemoryCacheFailed(t *testing.T) { ctx := context.Background() // Test Get with invalid type - cache, err := NewMemoryCache() - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - cache.store.Store("invalidKey", "invalidValue") - _, err = cache.Get(ctx, "invalidKey") - if err == nil { - t.Fatalf("expected error, got nil") - } + t.Run("GetWithInvalidType", func(t *testing.T) { + cache, err := NewMemoryCache() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + cache.store.Store("invalidKey", "invalidValue") + _, err = cache.Get(ctx, "invalidKey") + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + + t.Run("ValidateFailed", func(t *testing.T) { + cache, err := NewMemoryCache() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + bundle := &Bundle{ + BaseCRL: nil, + Metadata: Metadata{ + CreateAt: time.Now(), + BaseCRL: CRLMetadata{ + URL: "http://crl", + }, + }} + err = cache.Set(ctx, "invalidBundle", bundle) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) } From b8fb823e62d3bc36288cb290c0479bd6e83a08cf Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Tue, 20 Aug 2024 05:34:46 +0000 Subject: [PATCH 072/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 14 +++++++------- revocation/crl/cache/file.go | 4 ++-- revocation/crl/cache/file_test.go | 16 ++++++++-------- revocation/crl/cache/memory.go | 2 +- revocation/crl/cache/memory_test.go | 6 +++--- revocation/crl/fetcher/fetcher.go | 2 +- revocation/crl/fetcher/fetcher_test.go | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index a2135cb7..f14b947c 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -20,14 +20,14 @@ import ( ) const ( - // BaseCRL is the file name of the base CRL + // PathBaseCRL is the file name of the base CRL PathBaseCRL = "base.crl" - // Metadata is the file name of the metadata + // PathMetadata is the file name of the metadata PathMetadata = "metadata.json" ) -// CRLMetadata stores the URL and creation time of the file +// CRLMetadata stores the URL of the CRL type CRLMetadata struct { URL string `json:"url"` } @@ -39,11 +39,11 @@ type Metadata struct { // BaseCRL stores the URL of the base CRL BaseCRL CRLMetadata `json:"base.crl"` - // CreateAt stores the creation time of the CRL bundle. This is different + // CreatedAt stores the creation time of the CRL bundle. This is different // from the `ThisUpdate` field in the CRL. The `ThisUpdate` field in the CRL - // is the time when the CRL was generated, while the `CreateAt` field is for + // is the time when the CRL was generated, while the `CreatedAt` field is for // caching purpose, indicating the start of cache effective period. - CreateAt time.Time `json:"createAt"` + CreatedAt time.Time `json:"createAt"` } // Bundle is in memory representation of the Bundle tarball, including base CRL @@ -63,7 +63,7 @@ func (b *Bundle) Validate() error { if b.Metadata.BaseCRL.URL == "" { return errors.New("base CRL URL is missing") } - if b.Metadata.CreateAt.IsZero() { + if b.Metadata.CreatedAt.IsZero() { return errors.New("base CRL creation time is missing") } return nil diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index b5eaac49..6c77b7ab 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -95,7 +95,7 @@ func (c *FileCache) Get(ctx context.Context, uri string) (bundle *Bundle, err er return nil, err } - expires := bundle.Metadata.CreateAt.Add(c.MaxAge) + expires := bundle.Metadata.CreatedAt.Add(c.MaxAge) if c.MaxAge > 0 && time.Now().After(expires) { // do not delete the file to maintain the idempotent behavior return nil, ErrCacheMiss @@ -224,7 +224,7 @@ func saveTar(w io.Writer, bundle *Bundle) (err error) { }() // Add base.crl - if err := addToTar(PathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CreateAt, tarWriter); err != nil { + if err := addToTar(PathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CreatedAt, tarWriter); err != nil { return err } diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index 0353e063..97d63506 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -59,7 +59,7 @@ func TestFileCache(t *testing.T) { }) key := "testKey" - bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreateAt: time.Now()}} + bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreatedAt: time.Now()}} t.Run("SetAndGet", func(t *testing.T) { if err := cache.Set(ctx, key, bundle); err != nil { t.Fatalf("expected no error, got %v", err) @@ -68,13 +68,13 @@ func TestFileCache(t *testing.T) { if err != nil { t.Fatalf("expected no error, got %v", err) } - if retrievedBundle.Metadata.CreateAt.Unix() != bundle.Metadata.CreateAt.Unix() { + if retrievedBundle.Metadata.CreatedAt.Unix() != bundle.Metadata.CreatedAt.Unix() { t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) } }) t.Run("GetWithExpiredBundle", func(t *testing.T) { - expiredBundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreateAt: time.Now().Add(-DefaultMaxAge - 1*time.Second)}} + expiredBundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreatedAt: time.Now().Add(-DefaultMaxAge - 1*time.Second)}} if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { t.Fatalf("expected no error, got %v", err) } @@ -154,7 +154,7 @@ func TestGetFailed(t *testing.T) { t.Run("invalid bundle file", func(t *testing.T) { bundle := &Bundle{ BaseCRL: &x509.RevocationList{Raw: []byte("invalid crl")}, - Metadata: Metadata{CreateAt: time.Now()}, + Metadata: Metadata{CreatedAt: time.Now()}, } if err := saveTar(&bytes.Buffer{}, bundle); err != nil { t.Fatalf("expected no error, got %v", err) @@ -177,7 +177,7 @@ func TestSetFailed(t *testing.T) { } t.Run("failed to save tarball", func(t *testing.T) { - bundle := &Bundle{Metadata: Metadata{CreateAt: time.Now()}} + bundle := &Bundle{Metadata: Metadata{CreatedAt: time.Now()}} if err := cache.Set(context.Background(), "invalid.tar", bundle); err == nil { t.Fatalf("expected error, got nil") } @@ -207,7 +207,7 @@ func TestParseAndSave(t *testing.T) { BaseCRL: CRLMetadata{ URL: exampleURL, }, - CreateAt: time.Now(), + CreatedAt: time.Now(), }, } @@ -231,7 +231,7 @@ func TestParseAndSave(t *testing.T) { t.Errorf("expected URL to be %s, got %s", exampleURL, bundle.Metadata.BaseCRL.URL) } - if bundle.Metadata.CreateAt.IsZero() { + if bundle.Metadata.CreatedAt.IsZero() { t.Errorf("expected CreateAt to be set, got zero value") } }) @@ -328,7 +328,7 @@ func TestSaveTarFailed(t *testing.T) { BaseCRL: CRLMetadata{ URL: "https://example.com/base.crl", }, - CreateAt: time.Now(), + CreatedAt: time.Now(), }, } if err := saveTar(&errorWriter{}, bundle); err == nil { diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index e657713a..6f42ba8a 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -60,7 +60,7 @@ func (c *MemoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { return nil, fmt.Errorf("invalid type: %T", value) } - expires := bundle.Metadata.CreateAt.Add(c.MaxAge) + expires := bundle.Metadata.CreatedAt.Add(c.MaxAge) if c.MaxAge > 0 && time.Now().After(expires) { return nil, ErrCacheMiss } diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index fe6d33cc..06856289 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -52,7 +52,7 @@ func TestMemoryCache(t *testing.T) { bundle := &Bundle{ BaseCRL: baseCRL, Metadata: Metadata{ - CreateAt: time.Now(), + CreatedAt: time.Now(), BaseCRL: CRLMetadata{ URL: "http://crl", }, @@ -75,7 +75,7 @@ func TestMemoryCache(t *testing.T) { expiredBundle := &Bundle{ BaseCRL: baseCRL, Metadata: Metadata{ - CreateAt: time.Now().Add(-DefaultMaxAge - 1*time.Second), + CreatedAt: time.Now().Add(-DefaultMaxAge - 1*time.Second), BaseCRL: CRLMetadata{ URL: "http://crl", }, @@ -125,7 +125,7 @@ func TestMemoryCacheFailed(t *testing.T) { bundle := &Bundle{ BaseCRL: nil, Metadata: Metadata{ - CreateAt: time.Now(), + CreatedAt: time.Now(), BaseCRL: CRLMetadata{ URL: "http://crl", }, diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher/fetcher.go index ebfed139..2caf3e88 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -149,7 +149,7 @@ func download(ctx context.Context, crlURL string, client *http.Client) (bundle * BaseCRL: cache.CRLMetadata{ URL: crlURL, }, - CreateAt: time.Now(), + CreatedAt: time.Now(), }, }, nil } diff --git a/revocation/crl/fetcher/fetcher_test.go b/revocation/crl/fetcher/fetcher_test.go index 142d9f11..6f9dfcbc 100644 --- a/revocation/crl/fetcher/fetcher_test.go +++ b/revocation/crl/fetcher/fetcher_test.go @@ -77,7 +77,7 @@ func TestFetch(t *testing.T) { BaseCRL: cache.CRLMetadata{ URL: exampleURL, }, - CreateAt: time.Now(), + CreatedAt: time.Now(), }, } if err := c.Set(context.Background(), exampleURL, bundle); err != nil { From 291cdb79a30eff195b2e0a3a0af602ca85ca60a0 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Tue, 20 Aug 2024 06:05:02 +0000 Subject: [PATCH 073/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/file.go | 3 +-- revocation/crl/cache/file_test.go | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 6c77b7ab..16087235 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -115,11 +115,10 @@ func (c *FileCache) Set(ctx context.Context, uri string, bundle *Bundle) error { if err != nil { return err } - defer tempFile.Close() - if err := saveTar(tempFile, bundle); err != nil { return err } + tempFile.Close() // rename is atomic on UNIX-like platforms return os.Rename(tempFile.Name(), filepath.Join(c.root, fileName(uri))) diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index 97d63506..05d8c683 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -23,6 +23,7 @@ import ( "math/big" "os" "path/filepath" + "runtime" "strings" "testing" "time" @@ -92,6 +93,10 @@ func TestFileCache(t *testing.T) { func TestNewFileCache(t *testing.T) { tempDir := t.TempDir() t.Run("without permission to create cache directory", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping test on Windows") + } + if err := os.Chmod(tempDir, 0); err != nil { t.Fatalf("failed to change permission: %v", err) } From ec447303ee4f615bbe4d9e955f6e0556e4f3db99 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Sat, 14 Sep 2024 06:26:21 +0000 Subject: [PATCH 074/110] merge: CRL Signed-off-by: Junjie Gao --- revocation/internal/crl/crl.go | 288 ++++++++ revocation/internal/crl/crl_test.go | 685 ++++++++++++++++++ revocation/internal/crl/errors.go | 22 + revocation/{ => internal}/ocsp/errors.go | 0 revocation/{ => internal}/ocsp/errors_test.go | 0 revocation/internal/ocsp/ocsp.go | 242 +++++++ revocation/internal/ocsp/ocsp_test.go | 208 ++++++ revocation/internal/x509util/validate.go | 47 ++ revocation/internal/x509util/validate_test.go | 60 ++ revocation/ocsp/error.go | 37 + revocation/ocsp/ocsp.go | 228 +----- revocation/ocsp/ocsp_test.go | 208 ++---- revocation/result/results.go | 98 ++- revocation/result/results_test.go | 21 + revocation/revocation.go | 156 +++- revocation/revocation_test.go | 347 ++++++++- 16 files changed, 2254 insertions(+), 393 deletions(-) create mode 100644 revocation/internal/crl/crl.go create mode 100644 revocation/internal/crl/crl_test.go create mode 100644 revocation/internal/crl/errors.go rename revocation/{ => internal}/ocsp/errors.go (100%) rename revocation/{ => internal}/ocsp/errors_test.go (100%) create mode 100644 revocation/internal/ocsp/ocsp.go create mode 100644 revocation/internal/ocsp/ocsp_test.go create mode 100644 revocation/internal/x509util/validate.go create mode 100644 revocation/internal/x509util/validate_test.go create mode 100644 revocation/ocsp/error.go diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go new file mode 100644 index 00000000..48a5930a --- /dev/null +++ b/revocation/internal/crl/crl.go @@ -0,0 +1,288 @@ +// 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 crl provides methods for checking the revocation status of a +// certificate using CRL +package crl + +import ( + "context" + "crypto/x509" + "encoding/asn1" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/notaryproject/notation-core-go/revocation/result" +) + +var ( + // oidFreshestCRL is the object identifier for the distribution point + // for the delta CRL. (See RFC 5280, Section 5.2.6) + oidFreshestCRL = asn1.ObjectIdentifier{2, 5, 29, 46} + + // oidIssuingDistributionPoint is the object identifier for the issuing + // distribution point CRL extension. (See RFC 5280, Section 5.2.5) + oidIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28} + + // oidInvalidityDate is the object identifier for the invalidity date + // CRL entry extension. (See RFC 5280, Section 5.3.2) + oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} +) + +// maxCRLSize is the maximum size of CRL in bytes +// +// CRL examples: https://chasersystems.com/blog/an-analysis-of-certificate-revocation-list-sizes/ +const maxCRLSize = 32 * 1024 * 1024 // 32 MiB + +// CertCheckStatusOptions specifies values that are needed to check CRL +type CertCheckStatusOptions struct { + // HTTPClient is the HTTP client used to download CRL + HTTPClient *http.Client + + // SigningTime is used to compare with the invalidity date during revocation + // check + SigningTime time.Time +} + +// CertCheckStatus checks the revocation status of a certificate using CRL +// +// The function checks the revocation status of the certificate by downloading +// the CRL from the CRL distribution points specified in the certificate. +// +// If the invalidity date extension is present in the CRL entry and SigningTime +// is not zero, the certificate is considered revoked if the SigningTime is +// after the invalidity date. (See RFC 5280, Section 5.3.2) +func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts CertCheckStatusOptions) *result.CertRevocationResult { + if !Supported(cert) { + // CRL not enabled for this certificate. + return &result.CertRevocationResult{ + Result: result.ResultNonRevokable, + ServerResults: []*result.ServerResult{{ + RevocationMethod: result.RevocationMethodCRL, + Result: result.ResultNonRevokable, + }}, + RevocationMethod: result.RevocationMethodCRL, + } + } + + // The CRLDistributionPoints contains the URIs of all the CRL distribution + // points. Since it does not distinguish the reason field, it needs to check + // all the URIs to avoid missing any partial CRLs. + // + // For the majority of the certificates, there is only one CRL distribution + // point with one CRL URI, which will be cached, so checking all the URIs is + // not a performance issue. + var ( + serverResults = make([]*result.ServerResult, 0, len(cert.CRLDistributionPoints)) + lastErr error + crlURL string + ) + for _, crlURL = range cert.CRLDistributionPoints { + baseCRL, err := download(ctx, crlURL, opts.HTTPClient) + if err != nil { + lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err) + break + } + + if err = validate(baseCRL, issuer); err != nil { + lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err) + break + } + + crlResult, err := checkRevocation(cert, baseCRL, opts.SigningTime, crlURL) + if err != nil { + lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err) + break + } + if crlResult.Result == result.ResultRevoked { + return &result.CertRevocationResult{ + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{crlResult}, + RevocationMethod: result.RevocationMethodCRL, + } + } + + serverResults = append(serverResults, crlResult) + } + + if lastErr != nil { + return &result.CertRevocationResult{ + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{ + { + Result: result.ResultUnknown, + Server: crlURL, + Error: lastErr, + RevocationMethod: result.RevocationMethodCRL, + }}, + RevocationMethod: result.RevocationMethodCRL, + } + } + + return &result.CertRevocationResult{ + Result: result.ResultOK, + ServerResults: serverResults, + RevocationMethod: result.RevocationMethodCRL, + } +} + +// Supported checks if the certificate supports CRL. +func Supported(cert *x509.Certificate) bool { + return cert != nil && len(cert.CRLDistributionPoints) > 0 +} + +func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { + // check signature + if err := crl.CheckSignatureFrom(issuer); err != nil { + return fmt.Errorf("CRL is not signed by CA %s: %w,", issuer.Subject, err) + } + + // check validity + now := time.Now() + if !crl.NextUpdate.IsZero() && now.After(crl.NextUpdate) { + return fmt.Errorf("expired CRL. Current time %v is after CRL NextUpdate %v", now, crl.NextUpdate) + } + + for _, ext := range crl.Extensions { + switch { + case ext.Id.Equal(oidFreshestCRL): + return ErrDeltaCRLNotSupported + case ext.Id.Equal(oidIssuingDistributionPoint): + // IssuingDistributionPoint is a critical extension that identifies + // the scope of the CRL. Since we will check all the CRL + // distribution points, it is not necessary to check this extension. + default: + if ext.Critical { + // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) + return fmt.Errorf("unsupported critical extension found in CRL: %v", ext.Id) + } + } + } + + return nil +} + +// checkRevocation checks if the certificate is revoked or not +func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signingTime time.Time, crlURL string) (*result.ServerResult, error) { + if cert == nil { + return nil, errors.New("certificate cannot be nil") + } + + if baseCRL == nil { + return nil, errors.New("baseCRL cannot be nil") + } + + for _, revocationEntry := range baseCRL.RevokedCertificateEntries { + if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 { + extensions, err := parseEntryExtensions(revocationEntry) + if err != nil { + return nil, err + } + + // validate signingTime and invalidityDate + if !signingTime.IsZero() && !extensions.invalidityDate.IsZero() && + signingTime.Before(extensions.invalidityDate) { + // signing time is before the invalidity date which means the + // certificate is not revoked at the time of signing. + break + } + + // revoked + return &result.ServerResult{ + Result: result.ResultRevoked, + Server: crlURL, + RevocationMethod: result.RevocationMethodCRL, + }, nil + } + } + + return &result.ServerResult{ + Result: result.ResultOK, + Server: crlURL, + RevocationMethod: result.RevocationMethodCRL, + }, nil +} + +type entryExtensions struct { + // invalidityDate is the date when the key is invalid. + invalidityDate time.Time +} + +func parseEntryExtensions(entry x509.RevocationListEntry) (entryExtensions, error) { + var extensions entryExtensions + for _, ext := range entry.Extensions { + switch { + case ext.Id.Equal(oidInvalidityDate): + var invalidityDate time.Time + rest, err := asn1.UnmarshalWithParams(ext.Value, &invalidityDate, "generalized") + if err != nil { + return entryExtensions{}, fmt.Errorf("failed to parse invalidity date: %w", err) + } + if len(rest) > 0 { + return entryExtensions{}, fmt.Errorf("invalid invalidity date extension: trailing data") + } + + extensions.invalidityDate = invalidityDate + default: + if ext.Critical { + // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) + return entryExtensions{}, fmt.Errorf("unsupported critical extension found in CRL: %v", ext.Id) + } + } + } + + return extensions, nil +} + +func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { + // validate URL + parsedURL, err := url.Parse(crlURL) + if err != nil { + return nil, fmt.Errorf("invalid CRL URL: %w", err) + } + if parsedURL.Scheme != "http" { + return nil, fmt.Errorf("unsupported CRL endpoint: %s. Only urls with HTTP scheme is supported", crlURL) + } + + // download CRL + req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create CRL request %q: %w", crlURL, err) + } + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed for %q: %w", crlURL, err) + } + defer resp.Body.Close() + + // check response + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("%s %q: failed to download with status code: %d", resp.Request.Method, resp.Request.URL, resp.StatusCode) + } + + // read with size limit + limitedReader := io.LimitReader(resp.Body, maxCRLSize) + data, err := io.ReadAll(limitedReader) + if err != nil { + return nil, fmt.Errorf("failed to read CRL response from %q: %w", resp.Request.URL, err) + } + if len(data) == maxCRLSize { + return nil, fmt.Errorf("%s %q: CRL size reached the %d MiB size limit", resp.Request.Method, resp.Request.URL, maxCRLSize/1024/1024) + } + + return x509.ParseRevocationList(data) +} diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go new file mode 100644 index 00000000..2129fb79 --- /dev/null +++ b/revocation/internal/crl/crl_test.go @@ -0,0 +1,685 @@ +// 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 crl + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + "io" + "math/big" + "net/http" + "testing" + "time" + + "github.com/notaryproject/notation-core-go/revocation/result" + "github.com/notaryproject/notation-core-go/testhelper" +) + +func TestCertCheckStatus(t *testing.T) { + t.Run("certtificate does not have CRLDistributionPoints", func(t *testing.T) { + cert := &x509.Certificate{} + r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{}) + if r.Result != result.ResultNonRevokable { + t.Fatalf("expected NonRevokable, got %s", r.Result) + } + }) + + t.Run("download error", func(t *testing.T) { + cert := &x509.Certificate{ + CRLDistributionPoints: []string{"http://example.com"}, + } + r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: errorRoundTripperMock{}, + }, + }) + if r.ServerResults[0].Error == nil { + t.Fatal("expected error") + } + }) + + t.Run("CRL validate failed", func(t *testing.T) { + cert := &x509.Certificate{ + CRLDistributionPoints: []string{"http://example.com"}, + } + r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: expiredCRLRoundTripperMock{}, + }, + }) + if r.ServerResults[0].Error == nil { + t.Fatal("expected error") + } + }) + + // prepare a certificate chain + chain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + issuerCert := chain[1].Cert + issuerKey := chain[1].PrivateKey + + t.Run("revoked", func(t *testing.T) { + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: chain[0].Cert.SerialNumber, + RevocationTime: time.Now().Add(-time.Hour), + }, + }, + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: expectedRoundTripperMock{Body: crlBytes}, + }, + }) + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + + t.Run("unknown critical extension", func(t *testing.T) { + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: chain[0].Cert.SerialNumber, + RevocationTime: time.Now().Add(-time.Hour), + ExtraExtensions: []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: true, + }, + }, + }, + }, + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: expectedRoundTripperMock{Body: crlBytes}, + }, + }) + if r.ServerResults[0].Error == nil { + t.Fatal("expected error") + } + }) + + t.Run("Not revoked", func(t *testing.T) { + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: expectedRoundTripperMock{Body: crlBytes}, + }, + }) + if r.Result != result.ResultOK { + t.Fatalf("expected OK, got %s", r.Result) + } + }) + + t.Run("CRL with delta CRL is not checked", func(t *testing.T) { + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + ExtraExtensions: []pkix.Extension{ + { + Id: oidFreshestCRL, + Critical: false, + }, + }, + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: expectedRoundTripperMock{Body: crlBytes}, + }, + }) + if !errors.Is(r.ServerResults[0].Error, ErrDeltaCRLNotSupported) { + t.Fatal("expected ErrDeltaCRLNotChecked") + } + }) +} + +func TestValidate(t *testing.T) { + t.Run("expired CRL", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(1, false, true) + issuerCert := chain[0].Cert + issuerKey := chain[0].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(-time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatal(err) + } + + if err := validate(crl, issuerCert); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("check signature failed", func(t *testing.T) { + crl := &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + } + + if err := validate(crl, &x509.Certificate{}); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("unsupported CRL critical extensions", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(1, false, true) + issuerCert := chain[0].Cert + issuerKey := chain[0].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatal(err) + } + + // add unsupported critical extension + crl.Extensions = []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: true, + }, + } + + if err := validate(crl, issuerCert); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("issuing distribution point extension exists", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(1, false, true) + issuerCert := chain[0].Cert + issuerKey := chain[0].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + ExtraExtensions: []pkix.Extension{ + { + Id: oidIssuingDistributionPoint, + Critical: true, + }, + }, + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatal(err) + } + + if err := validate(crl, issuerCert); err != nil { + t.Fatal(err) + } + }) +} + +func TestCheckRevocation(t *testing.T) { + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + } + signingTime := time.Now() + + t.Run("certificate is nil", func(t *testing.T) { + _, err := checkRevocation(nil, &x509.RevocationList{}, signingTime, "") + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("CRL is nil", func(t *testing.T) { + _, err := checkRevocation(cert, nil, signingTime, "") + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("not revoked", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(2), + }, + }, + } + r, err := checkRevocation(cert, baseCRL, signingTime, "") + if err != nil { + t.Fatal(err) + } + if r.Result != result.ResultOK { + t.Fatalf("unexpected result, got %s", r.Result) + } + }) + + t.Run("revoked", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + RevocationTime: time.Now().Add(-time.Hour), + }, + }, + } + r, err := checkRevocation(cert, baseCRL, signingTime, "") + if err != nil { + t.Fatal(err) + } + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + + t.Run("revoked but signing time is before invalidityDate", func(t *testing.T) { + invalidityDate := time.Now().Add(time.Hour) + invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) + if err != nil { + t.Fatal(err) + } + + extensions := []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: invalidityDateBytes, + }, + } + + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + RevocationTime: time.Now().Add(time.Hour), + Extensions: extensions, + }, + }, + } + r, err := checkRevocation(cert, baseCRL, signingTime, "") + if err != nil { + t.Fatal(err) + } + if r.Result != result.ResultOK { + t.Fatalf("unexpected result, got %s", r.Result) + } + }) + + t.Run("revoked; signing time is after invalidityDate", func(t *testing.T) { + invalidityDate := time.Now().Add(-time.Hour) + invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) + if err != nil { + t.Fatal(err) + } + + extensions := []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: invalidityDateBytes, + }, + } + + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + RevocationTime: time.Now().Add(-time.Hour), + Extensions: extensions, + }, + }, + } + r, err := checkRevocation(cert, baseCRL, signingTime, "") + if err != nil { + t.Fatal(err) + } + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + + t.Run("revoked and signing time is zero", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + RevocationTime: time.Time{}, + }, + }, + } + r, err := checkRevocation(cert, baseCRL, time.Time{}, "") + if err != nil { + t.Fatal(err) + } + if r.Result != result.ResultRevoked { + t.Fatalf("expected revoked, got %s", r.Result) + } + }) + + t.Run("revocation entry validation error", func(t *testing.T) { + baseCRL := &x509.RevocationList{ + RevokedCertificateEntries: []x509.RevocationListEntry{ + { + SerialNumber: big.NewInt(1), + Extensions: []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: true, + }, + }, + }, + }, + } + _, err := checkRevocation(cert, baseCRL, signingTime, "") + if err == nil { + t.Fatal("expected error") + } + }) +} + +func TestParseEntryExtension(t *testing.T) { + t.Run("unsupported critical extension", func(t *testing.T) { + entry := x509.RevocationListEntry{ + Extensions: []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: true, + }, + }, + } + if _, err := parseEntryExtensions(entry); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("valid extension", func(t *testing.T) { + entry := x509.RevocationListEntry{ + Extensions: []pkix.Extension{ + { + Id: []int{1, 2, 3}, + Critical: false, + }, + }, + } + if _, err := parseEntryExtensions(entry); err != nil { + t.Fatal(err) + } + }) + + t.Run("parse invalidityDate", func(t *testing.T) { + + // create a time and marshal it to be generalizedTime + invalidityDate := time.Now() + invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) + if err != nil { + t.Fatal(err) + } + + entry := x509.RevocationListEntry{ + Extensions: []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: invalidityDateBytes, + }, + }, + } + extensions, err := parseEntryExtensions(entry) + if err != nil { + t.Fatal(err) + } + + if extensions.invalidityDate.IsZero() { + t.Fatal("expected invalidityDate") + } + }) + + t.Run("parse invalidityDate with error", func(t *testing.T) { + // invalid invalidityDate extension + entry := x509.RevocationListEntry{ + Extensions: []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: []byte{0x00, 0x01, 0x02, 0x03}, + }, + }, + } + _, err := parseEntryExtensions(entry) + if err == nil { + t.Fatal("expected error") + } + + // invalidityDate extension with extra bytes + invalidityDate := time.Now() + invalidityDateBytes, err := marshalGeneralizedTimeToBytes(invalidityDate) + if err != nil { + t.Fatal(err) + } + invalidityDateBytes = append(invalidityDateBytes, 0x00) + + entry = x509.RevocationListEntry{ + Extensions: []pkix.Extension{ + { + Id: oidInvalidityDate, + Critical: false, + Value: invalidityDateBytes, + }, + }, + } + _, err = parseEntryExtensions(entry) + if err == nil { + t.Fatal("expected error") + } + }) +} + +// marshalGeneralizedTimeToBytes converts a time.Time to ASN.1 GeneralizedTime bytes. +func marshalGeneralizedTimeToBytes(t time.Time) ([]byte, error) { + // ASN.1 GeneralizedTime requires the time to be in UTC + t = t.UTC() + // Use asn1.Marshal to directly get the ASN.1 GeneralizedTime bytes + return asn1.Marshal(t) +} + +func TestDownload(t *testing.T) { + t.Run("parse url error", func(t *testing.T) { + _, err := download(context.Background(), ":", http.DefaultClient) + if err == nil { + t.Fatal("expected error") + } + }) + t.Run("https download", func(t *testing.T) { + _, err := download(context.Background(), "https://example.com", http.DefaultClient) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("http.NewRequestWithContext error", func(t *testing.T) { + var ctx context.Context = nil + _, err := download(ctx, "http://example.com", &http.Client{}) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("client.Do error", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: errorRoundTripperMock{}, + }) + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("status code is not 2xx", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: serverErrorRoundTripperMock{}, + }) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("readAll error", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: readFailedRoundTripperMock{}, + }) + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("exceed the size limit", func(t *testing.T) { + _, err := download(context.Background(), "http://example.com", &http.Client{ + Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, + }) + if err == nil { + t.Fatal("expected error") + } + }) +} + +func TestSupported(t *testing.T) { + t.Run("supported", func(t *testing.T) { + cert := &x509.Certificate{ + CRLDistributionPoints: []string{"http://example.com"}, + } + if !Supported(cert) { + t.Fatal("expected supported") + } + }) + + t.Run("unsupported", func(t *testing.T) { + cert := &x509.Certificate{} + if Supported(cert) { + t.Fatal("expected unsupported") + } + }) +} + +type errorRoundTripperMock struct{} + +func (rt errorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("error") +} + +type serverErrorRoundTripperMock struct{} + +func (rt serverErrorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + Request: req, + StatusCode: http.StatusInternalServerError, + }, nil +} + +type readFailedRoundTripperMock struct{} + +func (rt readFailedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: errorReaderMock{}, + Request: &http.Request{ + Method: http.MethodGet, + URL: req.URL, + }, + }, nil +} + +type expiredCRLRoundTripperMock struct{} + +func (rt expiredCRLRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + chain := testhelper.GetRevokableRSAChainWithRevocations(1, false, true) + issuerCert := chain[0].Cert + issuerKey := chain[0].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(-time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(crlBytes)), + }, nil +} + +type errorReaderMock struct{} + +func (r errorReaderMock) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("error") +} + +func (r errorReaderMock) Close() error { + return nil +} + +type expectedRoundTripperMock struct { + Body []byte +} + +func (rt expectedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + Request: req, + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(rt.Body)), + }, nil +} diff --git a/revocation/internal/crl/errors.go b/revocation/internal/crl/errors.go new file mode 100644 index 00000000..37866551 --- /dev/null +++ b/revocation/internal/crl/errors.go @@ -0,0 +1,22 @@ +// 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 crl + +import "errors" + +var ( + // ErrDeltaCRLNotSupported is returned when the CRL contains a delta CRL but + // the delta CRL is not supported. + ErrDeltaCRLNotSupported = errors.New("delta CRL is not supported") +) diff --git a/revocation/ocsp/errors.go b/revocation/internal/ocsp/errors.go similarity index 100% rename from revocation/ocsp/errors.go rename to revocation/internal/ocsp/errors.go diff --git a/revocation/ocsp/errors_test.go b/revocation/internal/ocsp/errors_test.go similarity index 100% rename from revocation/ocsp/errors_test.go rename to revocation/internal/ocsp/errors_test.go diff --git a/revocation/internal/ocsp/ocsp.go b/revocation/internal/ocsp/ocsp.go new file mode 100644 index 00000000..25410ed3 --- /dev/null +++ b/revocation/internal/ocsp/ocsp.go @@ -0,0 +1,242 @@ +// 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 ocsp provides methods for checking the OCSP revocation status of a +// certificate chain, as well as errors related to these checks +package ocsp + +import ( + "bytes" + "crypto" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/notaryproject/notation-core-go/revocation/result" + "golang.org/x/crypto/ocsp" +) + +// CertCheckStatusOptions specifies values that are needed to check OCSP revocation +type CertCheckStatusOptions struct { + // HTTPClient is the HTTP client used to perform the OCSP request + HTTPClient *http.Client + + // SigningTime is used to compare with the invalidity date during revocation + SigningTime time.Time +} + +const ( + pkixNoCheckOID string = "1.3.6.1.5.5.7.48.1.5" + invalidityDateOID string = "2.5.29.24" + // Max size determined from https://www.ibm.com/docs/en/sva/9.0.6?topic=stanza-ocsp-max-response-size. + // Typical size is ~4 KB + ocspMaxResponseSize int64 = 20480 //bytes +) + +// CertCheckStatus checks the revocation status of a certificate using OCSP +func CertCheckStatus(cert, issuer *x509.Certificate, opts CertCheckStatusOptions) *result.CertRevocationResult { + if !Supported(cert) { + // OCSP not enabled for this certificate. + return &result.CertRevocationResult{ + Result: result.ResultNonRevokable, + ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})}, + RevocationMethod: result.RevocationMethodOCSP, + } + } + ocspURLs := cert.OCSPServer + + serverResults := make([]*result.ServerResult, len(ocspURLs)) + for serverIndex, server := range ocspURLs { + serverResult := checkStatusFromServer(cert, issuer, server, opts) + if serverResult.Result == result.ResultOK || + serverResult.Result == result.ResultRevoked || + (serverResult.Result == result.ResultUnknown && errors.Is(serverResult.Error, UnknownStatusError{})) { + // A valid response has been received from an OCSP server + // Result should be based on only this response, not any errors from + // other servers + return serverResultsToCertRevocationResult([]*result.ServerResult{serverResult}) + } + serverResults[serverIndex] = serverResult + } + return serverResultsToCertRevocationResult(serverResults) +} + +// Supported returns true if the certificate supports OCSP. +func Supported(cert *x509.Certificate) bool { + return cert != nil && len(cert.OCSPServer) > 0 +} + +func checkStatusFromServer(cert, issuer *x509.Certificate, server string, opts CertCheckStatusOptions) *result.ServerResult { + // Check valid server + if serverURL, err := url.Parse(server); err != nil || !strings.EqualFold(serverURL.Scheme, "http") { + // This function is only able to check servers that are accessible via HTTP + return toServerResult(server, GenericError{Err: fmt.Errorf("OCSPServer protocol %s is not supported", serverURL.Scheme)}) + } + + // Create OCSP Request + resp, err := executeOCSPCheck(cert, issuer, server, opts) + if err != nil { + // If there is a server error, attempt all servers before determining what to return + // to the user + return toServerResult(server, err) + } + + // Validate OCSP response isn't expired + if time.Now().After(resp.NextUpdate) { + return toServerResult(server, GenericError{Err: errors.New("expired OCSP response")}) + } + + // Handle pkix-ocsp-no-check and id-ce-invalidityDate extensions if present + // in response + extensionMap := extensionsToMap(resp.Extensions) + if _, foundNoCheck := extensionMap[pkixNoCheckOID]; !foundNoCheck { + // This will be ignored until CRL is implemented + // If it isn't found, CRL should be used to verify the OCSP response + _ = foundNoCheck // needed to bypass linter warnings (Remove after adding CRL) + // TODO: add CRL support + // https://github.com/notaryproject/notation-core-go/issues/125 + } + if invalidityDateBytes, foundInvalidityDate := extensionMap[invalidityDateOID]; foundInvalidityDate && !opts.SigningTime.IsZero() && resp.Status == ocsp.Revoked { + var invalidityDate time.Time + rest, err := asn1.UnmarshalWithParams(invalidityDateBytes, &invalidityDate, "generalized") + if len(rest) == 0 && err == nil && opts.SigningTime.Before(invalidityDate) { + return toServerResult(server, nil) + } + } + + // No errors, valid server response + switch resp.Status { + case ocsp.Good: + return toServerResult(server, nil) + case ocsp.Revoked: + return toServerResult(server, RevokedError{}) + default: + // ocsp.Unknown + return toServerResult(server, UnknownStatusError{}) + } +} + +func extensionsToMap(extensions []pkix.Extension) map[string][]byte { + extensionMap := make(map[string][]byte) + for _, extension := range extensions { + extensionMap[extension.Id.String()] = extension.Value + } + return extensionMap +} + +func executeOCSPCheck(cert, issuer *x509.Certificate, server string, opts CertCheckStatusOptions) (*ocsp.Response, error) { + // TODO: Look into other alternatives for specifying the Hash + // https://github.com/notaryproject/notation-core-go/issues/139 + // The following do not support SHA256 hashes: + // - Microsoft + // - Entrust + // - Let's Encrypt + // - Digicert (sometimes) + // As this represents a large percentage of public CAs, we are using the + // hashing algorithm SHA1, which has been confirmed to be supported by all + // that were tested. + ocspRequest, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA1}) + if err != nil { + return nil, GenericError{Err: err} + } + + var resp *http.Response + postRequired := base64.StdEncoding.EncodedLen(len(ocspRequest)) >= 255 + if !postRequired { + encodedReq := url.QueryEscape(base64.StdEncoding.EncodeToString(ocspRequest)) + if len(encodedReq) < 255 { + var reqURL string + reqURL, err = url.JoinPath(server, encodedReq) + if err != nil { + return nil, GenericError{Err: err} + } + resp, err = opts.HTTPClient.Get(reqURL) + } else { + resp, err = postRequest(ocspRequest, server, opts.HTTPClient) + } + } else { + resp, err = postRequest(ocspRequest, server, opts.HTTPClient) + } + + if err != nil { + var urlErr *url.Error + if errors.As(err, &urlErr) && urlErr.Timeout() { + return nil, TimeoutError{} + } + return nil, GenericError{Err: err} + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("failed to retrieve OCSP: response had status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, ocspMaxResponseSize)) + if err != nil { + return nil, GenericError{Err: err} + } + + switch { + case bytes.Equal(body, ocsp.UnauthorizedErrorResponse): + return nil, GenericError{Err: errors.New("OCSP unauthorized")} + case bytes.Equal(body, ocsp.MalformedRequestErrorResponse): + return nil, GenericError{Err: errors.New("OCSP malformed")} + case bytes.Equal(body, ocsp.InternalErrorErrorResponse): + return nil, GenericError{Err: errors.New("OCSP internal error")} + case bytes.Equal(body, ocsp.TryLaterErrorResponse): + return nil, GenericError{Err: errors.New("OCSP try later")} + case bytes.Equal(body, ocsp.SigRequredErrorResponse): + return nil, GenericError{Err: errors.New("OCSP signature required")} + } + + return ocsp.ParseResponseForCert(body, cert, issuer) +} + +func postRequest(req []byte, server string, httpClient *http.Client) (*http.Response, error) { + reader := bytes.NewReader(req) + return httpClient.Post(server, "application/ocsp-request", reader) +} + +func toServerResult(server string, err error) *result.ServerResult { + var serverResult *result.ServerResult + switch t := err.(type) { + case nil: + serverResult = result.NewServerResult(result.ResultOK, server, nil) + case NoServerError: + serverResult = result.NewServerResult(result.ResultNonRevokable, server, nil) + case RevokedError: + serverResult = result.NewServerResult(result.ResultRevoked, server, t) + default: + // Includes GenericError, UnknownStatusError, result.InvalidChainError, + // and TimeoutError + serverResult = result.NewServerResult(result.ResultUnknown, server, t) + } + serverResult.RevocationMethod = result.RevocationMethodOCSP + return serverResult +} + +func serverResultsToCertRevocationResult(serverResults []*result.ServerResult) *result.CertRevocationResult { + return &result.CertRevocationResult{ + Result: serverResults[len(serverResults)-1].Result, + ServerResults: serverResults, + RevocationMethod: result.RevocationMethodOCSP, + } +} diff --git a/revocation/internal/ocsp/ocsp_test.go b/revocation/internal/ocsp/ocsp_test.go new file mode 100644 index 00000000..f1f0f118 --- /dev/null +++ b/revocation/internal/ocsp/ocsp_test.go @@ -0,0 +1,208 @@ +// 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 ocsp + +import ( + "crypto/x509" + "fmt" + "net/http" + "strings" + "testing" + "time" + + "github.com/notaryproject/notation-core-go/revocation/result" + "github.com/notaryproject/notation-core-go/testhelper" + "golang.org/x/crypto/ocsp" +) + +func validateEquivalentCertResults(certResults, expectedCertResults []*result.CertRevocationResult, t *testing.T) { + if len(certResults) != len(expectedCertResults) { + t.Errorf("Length of certResults (%d) did not match expected length (%d)", len(certResults), len(expectedCertResults)) + return + } + for i, certResult := range certResults { + if certResult.Result != expectedCertResults[i].Result { + t.Errorf("Expected certResults[%d].Result to be %s, but got %s", i, expectedCertResults[i].Result, certResult.Result) + } + if len(certResult.ServerResults) != len(expectedCertResults[i].ServerResults) { + t.Errorf("Length of certResults[%d].ServerResults (%d) did not match expected length (%d)", i, len(certResult.ServerResults), len(expectedCertResults[i].ServerResults)) + return + } + for j, serverResult := range certResult.ServerResults { + if serverResult.Result != expectedCertResults[i].ServerResults[j].Result { + t.Errorf("Expected certResults[%d].ServerResults[%d].Result to be %s, but got %s", i, j, expectedCertResults[i].ServerResults[j].Result, serverResult.Result) + } + if serverResult.Server != expectedCertResults[i].ServerResults[j].Server { + t.Errorf("Expected certResults[%d].ServerResults[%d].Server to be %s, but got %s", i, j, expectedCertResults[i].ServerResults[j].Server, serverResult.Server) + } + if serverResult.Error == nil { + if expectedCertResults[i].ServerResults[j].Error == nil { + continue + } + t.Errorf("certResults[%d].ServerResults[%d].Error was nil, but expected %v", i, j, expectedCertResults[i].ServerResults[j].Error) + } else if expectedCertResults[i].ServerResults[j].Error == nil { + t.Errorf("Unexpected error for certResults[%d].ServerResults[%d].Error: %v", i, j, serverResult.Error) + } else if serverResult.Error.Error() != expectedCertResults[i].ServerResults[j].Error.Error() { + t.Errorf("Expected certResults[%d].ServerResults[%d].Error to be %v, but got %v", i, j, expectedCertResults[i].ServerResults[j].Error, serverResult.Error) + } + } + } +} + +func getOKCertResult(server string) *result.CertRevocationResult { + return &result.CertRevocationResult{ + Result: result.ResultOK, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultOK, server, nil), + }, + } +} + +func TestCheckStatus(t *testing.T) { + revokableCertTuple := testhelper.GetRevokableRSALeafCertificate() + revokableIssuerTuple := testhelper.GetRSARootCertificate() + ocspServer := revokableCertTuple.Cert.OCSPServer[0] + revokableChain := []*x509.Certificate{revokableCertTuple.Cert, revokableIssuerTuple.Cert} + testChain := []testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple} + + t.Run("check non-revoked cert", func(t *testing.T) { + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) + opts := CertCheckStatusOptions{ + SigningTime: time.Now(), + HTTPClient: client, + } + + certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) + expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} + validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) + }) + t.Run("check cert with Unknown OCSP response", func(t *testing.T) { + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Unknown}, nil, true) + opts := CertCheckStatusOptions{ + SigningTime: time.Now(), + HTTPClient: client, + } + + certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) + expectedCertResults := []*result.CertRevocationResult{{ + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultUnknown, ocspServer, UnknownStatusError{}), + }, + }} + validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) + }) + t.Run("check OCSP revoked cert", func(t *testing.T) { + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Revoked}, nil, true) + opts := CertCheckStatusOptions{ + SigningTime: time.Now(), + HTTPClient: client, + } + + certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) + expectedCertResults := []*result.CertRevocationResult{{ + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, ocspServer, RevokedError{}), + }, + }} + validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) + }) + t.Run("check OCSP future revoked cert", func(t *testing.T) { + revokedTime := time.Now().Add(time.Hour) + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Revoked}, &revokedTime, true) + opts := CertCheckStatusOptions{ + SigningTime: time.Now(), + HTTPClient: client, + } + + certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) + expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} + validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) + }) + + t.Run("certificate doesn't support OCSP", func(t *testing.T) { + ocspResult := CertCheckStatus(&x509.Certificate{}, revokableIssuerTuple.Cert, CertCheckStatusOptions{}) + expectedResult := &result.CertRevocationResult{ + Result: result.ResultNonRevokable, + ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})}, + } + + validateEquivalentCertResults([]*result.CertRevocationResult{ocspResult}, []*result.CertRevocationResult{expectedResult}, t) + }) +} + +func TestCheckStatusFromServer(t *testing.T) { + revokableCertTuple := testhelper.GetRevokableRSALeafCertificate() + revokableIssuerTuple := testhelper.GetRSARootCertificate() + + t.Run("server url is not http", func(t *testing.T) { + server := "https://example.com" + serverResult := checkStatusFromServer(revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{}) + expectedResult := toServerResult(server, GenericError{Err: fmt.Errorf("OCSPServer protocol %s is not supported", "https")}) + if serverResult.Result != expectedResult.Result { + t.Errorf("Expected Result to be %s, but got %s", expectedResult.Result, serverResult.Result) + } + if serverResult.Server != expectedResult.Server { + t.Errorf("Expected Server to be %s, but got %s", expectedResult.Server, serverResult.Server) + } + if serverResult.Error == nil { + t.Errorf("Expected Error to be %v, but got nil", expectedResult.Error) + } else if serverResult.Error.Error() != expectedResult.Error.Error() { + t.Errorf("Expected Error to be %v, but got %v", expectedResult.Error, serverResult.Error) + } + }) + + t.Run("request error", func(t *testing.T) { + server := "http://example.com" + serverResult := checkStatusFromServer(revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: &failedTransport{}, + }, + }) + errorMessage := "failed to execute request" + if !strings.Contains(serverResult.Error.Error(), errorMessage) { + t.Errorf("Expected Error to contain %v, but got %v", errorMessage, serverResult.Error) + } + }) + + t.Run("ocsp expired", func(t *testing.T) { + client := testhelper.MockClient([]testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) + server := "http://example.com/expired_ocsp" + serverResult := checkStatusFromServer(revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{ + HTTPClient: client, + }) + errorMessage := "expired OCSP response" + if !strings.Contains(serverResult.Error.Error(), errorMessage) { + t.Errorf("Expected Error to contain %v, but got %v", errorMessage, serverResult.Error) + } + }) +} + +func TestPostRequest(t *testing.T) { + t.Run("failed to execute request", func(t *testing.T) { + _, err := postRequest(nil, "http://example.com", &http.Client{ + Transport: &failedTransport{}, + }) + if err == nil { + t.Errorf("Expected error, but got nil") + } + }) +} + +type failedTransport struct{} + +func (f *failedTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("failed to execute request") +} diff --git a/revocation/internal/x509util/validate.go b/revocation/internal/x509util/validate.go new file mode 100644 index 00000000..134ef22b --- /dev/null +++ b/revocation/internal/x509util/validate.go @@ -0,0 +1,47 @@ +// 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 x509util provides the method to validate the certificate chain for a +// specific purpose, including code signing and timestamping. +package x509util + +import ( + "crypto/x509" + "fmt" + + "github.com/notaryproject/notation-core-go/revocation/purpose" + "github.com/notaryproject/notation-core-go/revocation/result" + coreX509 "github.com/notaryproject/notation-core-go/x509" +) + +// ValidateChain checks the certificate chain for a specific purpose, including +// code signing and timestamping. +func ValidateChain(certChain []*x509.Certificate, certChainPurpose purpose.Purpose) error { + switch certChainPurpose { + case purpose.CodeSigning: + // Since ValidateCodeSigningCertChain is using authentic signing time, + // signing time may be zero. + // Thus, it is better to pass nil here than fail for a cert's NotBefore + // being after zero time + if err := coreX509.ValidateCodeSigningCertChain(certChain, nil); err != nil { + return result.InvalidChainError{Err: err} + } + case purpose.Timestamping: + if err := coreX509.ValidateTimestampingCertChain(certChain); err != nil { + return result.InvalidChainError{Err: err} + } + default: + return result.InvalidChainError{Err: fmt.Errorf("unsupported certificate chain purpose %v", certChainPurpose)} + } + return nil +} diff --git a/revocation/internal/x509util/validate_test.go b/revocation/internal/x509util/validate_test.go new file mode 100644 index 00000000..22022e36 --- /dev/null +++ b/revocation/internal/x509util/validate_test.go @@ -0,0 +1,60 @@ +// 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 x509util + +import ( + "crypto/x509" + "testing" + + "github.com/notaryproject/notation-core-go/revocation/purpose" + "github.com/notaryproject/notation-core-go/testhelper" +) + +func TestValidate(t *testing.T) { + t.Run("unsupported_certificate_chain_purpose", func(t *testing.T) { + certChain := []*x509.Certificate{} + certChainPurpose := purpose.Purpose(-1) + err := ValidateChain(certChain, certChainPurpose) + if err == nil { + t.Errorf("Validate() failed, expected error, got nil") + } + }) + + t.Run("invalid code signing certificate chain", func(t *testing.T) { + certChain := []*x509.Certificate{} + certChainPurpose := purpose.CodeSigning + err := ValidateChain(certChain, certChainPurpose) + if err == nil { + t.Errorf("Validate() failed, expected error, got nil") + } + }) + + t.Run("invalid timestamping certificate chain", func(t *testing.T) { + certChain := []*x509.Certificate{} + certChainPurpose := purpose.Timestamping + err := ValidateChain(certChain, certChainPurpose) + if err == nil { + t.Errorf("Validate() failed, expected error, got nil") + } + }) + + t.Run("valid code signing certificate chain", func(t *testing.T) { + certChain := testhelper.GetRevokableRSAChain(2) + certChainPurpose := purpose.CodeSigning + err := ValidateChain([]*x509.Certificate{certChain[0].Cert, certChain[1].Cert}, certChainPurpose) + if err != nil { + t.Errorf("Validate() failed, expected nil, got %v", err) + } + }) +} diff --git a/revocation/ocsp/error.go b/revocation/ocsp/error.go new file mode 100644 index 00000000..005c700f --- /dev/null +++ b/revocation/ocsp/error.go @@ -0,0 +1,37 @@ +// 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 ocsp + +import "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" + +type ( + // RevokedError is returned when the certificate's status for OCSP is + // ocsp.Revoked + RevokedError = ocsp.RevokedError + + // UnknownStatusError is returned when the certificate's status for OCSP is + // ocsp.Unknown + UnknownStatusError = ocsp.UnknownStatusError + + // GenericError is returned when there is an error during the OCSP revocation + // check, not necessarily a revocation + GenericError = ocsp.GenericError + + // NoServerError is returned when the OCSPServer is not specified. + NoServerError = ocsp.NoServerError + + // TimeoutError is returned when the connection attempt to an OCSP URL exceeds + // the specified threshold + TimeoutError = ocsp.TimeoutError +) diff --git a/revocation/ocsp/ocsp.go b/revocation/ocsp/ocsp.go index cb9e97cc..c2274f75 100644 --- a/revocation/ocsp/ocsp.go +++ b/revocation/ocsp/ocsp.go @@ -16,25 +16,16 @@ package ocsp import ( - "bytes" - "crypto" "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/base64" "errors" - "fmt" - "io" "net/http" - "net/url" - "strings" "sync" "time" + "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" + "github.com/notaryproject/notation-core-go/revocation/internal/x509util" "github.com/notaryproject/notation-core-go/revocation/purpose" "github.com/notaryproject/notation-core-go/revocation/result" - coreX509 "github.com/notaryproject/notation-core-go/x509" - "golang.org/x/crypto/ocsp" ) // Options specifies values that are needed to check OCSP revocation @@ -45,19 +36,10 @@ type Options struct { // values are CodeSigning and Timestamping. // When not provided, the default value is CodeSigning. CertChainPurpose purpose.Purpose - - SigningTime time.Time - HTTPClient *http.Client + SigningTime time.Time + HTTPClient *http.Client } -const ( - pkixNoCheckOID string = "1.3.6.1.5.5.7.48.1.5" - invalidityDateOID string = "2.5.29.24" - // Max size determined from https://www.ibm.com/docs/en/sva/9.0.6?topic=stanza-ocsp-max-response-size. - // Typical size is ~4 KB - ocspMaxResponseSize int64 = 20480 //bytes -) - // CheckStatus checks OCSP based on the passed options and returns an array of // result.CertRevocationResult objects that contains the results and error. The // length of this array will always be equal to the length of the certificate @@ -67,24 +49,15 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} } - switch opts.CertChainPurpose { - case purpose.CodeSigning: - // Since ValidateCodeSigningCertChain is using authentic signing time, - // signing time may be zero. - // Thus, it is better to pass nil here than fail for a cert's NotBefore - // being after zero time - if err := coreX509.ValidateCodeSigningCertChain(opts.CertChain, nil); err != nil { - return nil, result.InvalidChainError{Err: err} - } - case purpose.Timestamping: - if err := coreX509.ValidateTimestampingCertChain(opts.CertChain); err != nil { - return nil, result.InvalidChainError{Err: err} - } - default: - return nil, result.InvalidChainError{Err: fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose)} + if err := x509util.ValidateChain(opts.CertChain, opts.CertChainPurpose); err != nil { + return nil, err } certResults := make([]*result.CertRevocationResult, len(opts.CertChain)) + certCheckStatusOptions := ocsp.CertCheckStatusOptions{ + SigningTime: opts.SigningTime, + HTTPClient: opts.HTTPClient, + } // Check status for each cert in cert chain var wg sync.WaitGroup @@ -93,7 +66,7 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { // Assume cert chain is accurate and next cert in chain is the issuer go func(i int, cert *x509.Certificate) { defer wg.Done() - certResults[i] = certCheckStatus(cert, opts.CertChain[i+1], opts) + certResults[i] = ocsp.CertCheckStatus(cert, opts.CertChain[i+1], certCheckStatusOptions) }(i, cert) } // Last is root cert, which will never be revoked by OCSP @@ -108,182 +81,3 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { wg.Wait() return certResults, nil } - -func certCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { - ocspURLs := cert.OCSPServer - if len(ocspURLs) == 0 { - // OCSP not enabled for this certificate. - return &result.CertRevocationResult{ - Result: result.ResultNonRevokable, - ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})}, - } - } - - serverResults := make([]*result.ServerResult, len(ocspURLs)) - for serverIndex, server := range ocspURLs { - serverResult := checkStatusFromServer(cert, issuer, server, opts) - if serverResult.Result == result.ResultOK || - serverResult.Result == result.ResultRevoked || - (serverResult.Result == result.ResultUnknown && errors.Is(serverResult.Error, UnknownStatusError{})) { - // A valid response has been received from an OCSP server - // Result should be based on only this response, not any errors from - // other servers - return serverResultsToCertRevocationResult([]*result.ServerResult{serverResult}) - } - serverResults[serverIndex] = serverResult - } - return serverResultsToCertRevocationResult(serverResults) -} - -func checkStatusFromServer(cert, issuer *x509.Certificate, server string, opts Options) *result.ServerResult { - // Check valid server - if serverURL, err := url.Parse(server); err != nil || !strings.EqualFold(serverURL.Scheme, "http") { - // This function is only able to check servers that are accessible via HTTP - return toServerResult(server, GenericError{Err: fmt.Errorf("OCSPServer protocol %s is not supported", serverURL.Scheme)}) - } - - // Create OCSP Request - resp, err := executeOCSPCheck(cert, issuer, server, opts) - if err != nil { - // If there is a server error, attempt all servers before determining what to return - // to the user - return toServerResult(server, err) - } - - // Validate OCSP response isn't expired - if time.Now().After(resp.NextUpdate) { - return toServerResult(server, GenericError{Err: errors.New("expired OCSP response")}) - } - - // Handle pkix-ocsp-no-check and id-ce-invalidityDate extensions if present - // in response - extensionMap := extensionsToMap(resp.Extensions) - if _, foundNoCheck := extensionMap[pkixNoCheckOID]; !foundNoCheck { - // This will be ignored until CRL is implemented - // If it isn't found, CRL should be used to verify the OCSP response - _ = foundNoCheck // needed to bypass linter warnings (Remove after adding CRL) - // TODO: add CRL support - // https://github.com/notaryproject/notation-core-go/issues/125 - } - if invalidityDateBytes, foundInvalidityDate := extensionMap[invalidityDateOID]; foundInvalidityDate && !opts.SigningTime.IsZero() && resp.Status == ocsp.Revoked { - var invalidityDate time.Time - rest, err := asn1.UnmarshalWithParams(invalidityDateBytes, &invalidityDate, "generalized") - if len(rest) == 0 && err == nil && opts.SigningTime.Before(invalidityDate) { - return toServerResult(server, nil) - } - } - - // No errors, valid server response - switch resp.Status { - case ocsp.Good: - return toServerResult(server, nil) - case ocsp.Revoked: - return toServerResult(server, RevokedError{}) - default: - // ocsp.Unknown - return toServerResult(server, UnknownStatusError{}) - } -} - -func extensionsToMap(extensions []pkix.Extension) map[string][]byte { - extensionMap := make(map[string][]byte) - for _, extension := range extensions { - extensionMap[extension.Id.String()] = extension.Value - } - return extensionMap -} - -func executeOCSPCheck(cert, issuer *x509.Certificate, server string, opts Options) (*ocsp.Response, error) { - // TODO: Look into other alternatives for specifying the Hash - // https://github.com/notaryproject/notation-core-go/issues/139 - // The following do not support SHA256 hashes: - // - Microsoft - // - Entrust - // - Let's Encrypt - // - Digicert (sometimes) - // As this represents a large percentage of public CAs, we are using the - // hashing algorithm SHA1, which has been confirmed to be supported by all - // that were tested. - ocspRequest, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA1}) - if err != nil { - return nil, GenericError{Err: err} - } - - var resp *http.Response - postRequired := base64.StdEncoding.EncodedLen(len(ocspRequest)) >= 255 - if !postRequired { - encodedReq := url.QueryEscape(base64.StdEncoding.EncodeToString(ocspRequest)) - if len(encodedReq) < 255 { - var reqURL string - reqURL, err = url.JoinPath(server, encodedReq) - if err != nil { - return nil, GenericError{Err: err} - } - resp, err = opts.HTTPClient.Get(reqURL) - } else { - resp, err = postRequest(ocspRequest, server, opts.HTTPClient) - } - } else { - resp, err = postRequest(ocspRequest, server, opts.HTTPClient) - } - - if err != nil { - var urlErr *url.Error - if errors.As(err, &urlErr) && urlErr.Timeout() { - return nil, TimeoutError{} - } - return nil, GenericError{Err: err} - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("failed to retrieve OCSP: response had status code %d", resp.StatusCode) - } - - body, err := io.ReadAll(io.LimitReader(resp.Body, ocspMaxResponseSize)) - if err != nil { - return nil, GenericError{Err: err} - } - - switch { - case bytes.Equal(body, ocsp.UnauthorizedErrorResponse): - return nil, GenericError{Err: errors.New("OCSP unauthorized")} - case bytes.Equal(body, ocsp.MalformedRequestErrorResponse): - return nil, GenericError{Err: errors.New("OCSP malformed")} - case bytes.Equal(body, ocsp.InternalErrorErrorResponse): - return nil, GenericError{Err: errors.New("OCSP internal error")} - case bytes.Equal(body, ocsp.TryLaterErrorResponse): - return nil, GenericError{Err: errors.New("OCSP try later")} - case bytes.Equal(body, ocsp.SigRequredErrorResponse): - return nil, GenericError{Err: errors.New("OCSP signature required")} - } - - return ocsp.ParseResponseForCert(body, cert, issuer) -} - -func postRequest(req []byte, server string, httpClient *http.Client) (*http.Response, error) { - reader := bytes.NewReader(req) - return httpClient.Post(server, "application/ocsp-request", reader) -} - -func toServerResult(server string, err error) *result.ServerResult { - switch t := err.(type) { - case nil: - return result.NewServerResult(result.ResultOK, server, nil) - case NoServerError: - return result.NewServerResult(result.ResultNonRevokable, server, nil) - case RevokedError: - return result.NewServerResult(result.ResultRevoked, server, t) - default: - // Includes GenericError, UnknownStatusError, result.InvalidChainError, - // and TimeoutError - return result.NewServerResult(result.ResultUnknown, server, t) - } -} - -func serverResultsToCertRevocationResult(serverResults []*result.ServerResult) *result.CertRevocationResult { - return &result.CertRevocationResult{ - Result: serverResults[len(serverResults)-1].Result, - ServerResults: serverResults, - } -} diff --git a/revocation/ocsp/ocsp_test.go b/revocation/ocsp/ocsp_test.go index 8a1479da..f677e6de 100644 --- a/revocation/ocsp/ocsp_test.go +++ b/revocation/ocsp/ocsp_test.go @@ -79,81 +79,14 @@ func getRootCertResult() *result.CertRevocationResult { } } -func TestCheckStatus(t *testing.T) { - revokableCertTuple := testhelper.GetRevokableRSALeafCertificate() - revokableIssuerTuple := testhelper.GetRSARootCertificate() - ocspServer := revokableCertTuple.Cert.OCSPServer[0] - revokableChain := []*x509.Certificate{revokableCertTuple.Cert, revokableIssuerTuple.Cert} - testChain := []testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple} - - t.Run("check non-revoked cert", func(t *testing.T) { - client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) - opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, - } - - certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) - expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} - validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) - }) - t.Run("check cert with Unknown OCSP response", func(t *testing.T) { - client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Unknown}, nil, true) - opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, - } - - certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) - expectedCertResults := []*result.CertRevocationResult{{ - Result: result.ResultUnknown, - ServerResults: []*result.ServerResult{ - result.NewServerResult(result.ResultUnknown, ocspServer, UnknownStatusError{}), - }, - }} - validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) - }) - t.Run("check OCSP revoked cert", func(t *testing.T) { - client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Revoked}, nil, true) - opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, - } - - certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) - expectedCertResults := []*result.CertRevocationResult{{ - Result: result.ResultRevoked, - ServerResults: []*result.ServerResult{ - result.NewServerResult(result.ResultRevoked, ocspServer, RevokedError{}), - }, - }} - validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) - }) - t.Run("check OCSP future revoked cert", func(t *testing.T) { - revokedTime := time.Now().Add(time.Hour) - client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Revoked}, &revokedTime, true) - opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, - } - - certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) - expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} - validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) - }) -} - func TestCheckStatusForSelfSignedCert(t *testing.T) { selfSignedTuple := testhelper.GetRSASelfSignedSigningCertTuple("Notation revocation test self-signed cert") client := testhelper.MockClient([]testhelper.RSACertTuple{selfSignedTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ - CertChain: []*x509.Certificate{selfSignedTuple.Cert}, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: []*x509.Certificate{selfSignedTuple.Cert}, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -168,9 +101,10 @@ func TestCheckStatusForRootCert(t *testing.T) { rootTuple := testhelper.GetRSARootCertificate() client := testhelper.MockClient([]testhelper.RSACertTuple{rootTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ - CertChain: []*x509.Certificate{rootTuple.Cert}, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: []*x509.Certificate{rootTuple.Cert}, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -187,9 +121,10 @@ func TestCheckStatusForNonSelfSignedSingleCert(t *testing.T) { certTuple := testhelper.GetRSALeafCertificate() client := testhelper.MockClient([]testhelper.RSACertTuple{certTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ - CertChain: []*x509.Certificate{certTuple.Cert}, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: []*x509.Certificate{certTuple.Cert}, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -213,9 +148,10 @@ func TestCheckStatusForChain(t *testing.T) { t.Run("empty chain", func(t *testing.T) { opts := Options{ - CertChain: []*x509.Certificate{}, - SigningTime: time.Now(), - HTTPClient: http.DefaultClient, + CertChain: []*x509.Certificate{}, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) expectedErr := result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} @@ -253,9 +189,10 @@ func TestCheckStatusForChain(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good}, nil, true) // 3rd cert will be unknown, the rest will be good opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: revokableChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -281,9 +218,10 @@ func TestCheckStatusForChain(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be revoked, the rest will be good opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: revokableChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -309,9 +247,10 @@ func TestCheckStatusForChain(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be unknown, 5th will be revoked, the rest will be good opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: revokableChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -343,9 +282,10 @@ func TestCheckStatusForChain(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be revoked, the rest will be good opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: revokableChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -367,9 +307,10 @@ func TestCheckStatusForChain(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be unknown, 5th will be revoked, the rest will be good opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: revokableChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -395,9 +336,10 @@ func TestCheckStatusForChain(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be revoked, the rest will be good opts := Options{ - CertChain: revokableChain, - SigningTime: time.Now().Add(time.Hour), - HTTPClient: client, + CertChain: revokableChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now().Add(time.Hour), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -424,9 +366,10 @@ func TestCheckStatusForChain(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be revoked, the rest will be good opts := Options{ - CertChain: revokableChain, - SigningTime: zeroTime, - HTTPClient: client, + CertChain: revokableChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: zeroTime, + HTTPClient: client, } if !zeroTime.IsZero() { @@ -484,9 +427,10 @@ func TestCheckStatusErrors(t *testing.T) { t.Run("no OCSPServer specified", func(t *testing.T) { opts := Options{ - CertChain: noOCSPChain, - SigningTime: time.Now(), - HTTPClient: http.DefaultClient, + CertChain: noOCSPChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) if err != nil { @@ -506,9 +450,10 @@ func TestCheckStatusErrors(t *testing.T) { t.Run("chain missing root", func(t *testing.T) { opts := Options{ - CertChain: noRootChain, - SigningTime: time.Now(), - HTTPClient: http.DefaultClient, + CertChain: noRootChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != chainRootErr.Error() { @@ -521,9 +466,10 @@ func TestCheckStatusErrors(t *testing.T) { t.Run("backwards chain", func(t *testing.T) { opts := Options{ - CertChain: backwardsChain, - SigningTime: time.Now(), - HTTPClient: http.DefaultClient, + CertChain: backwardsChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != backwardsChainErr.Error() { @@ -581,9 +527,10 @@ func TestCheckStatusErrors(t *testing.T) { t.Run("timeout", func(t *testing.T) { timeoutClient := &http.Client{Timeout: 1 * time.Nanosecond} opts := Options{ - CertChain: okChain, - SigningTime: time.Now(), - HTTPClient: timeoutClient, + CertChain: okChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: timeoutClient, } certResults, err := CheckStatus(opts) if err != nil { @@ -610,9 +557,10 @@ func TestCheckStatusErrors(t *testing.T) { t.Run("expired ocsp response", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ - CertChain: expiredChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: expiredChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { @@ -634,9 +582,10 @@ func TestCheckStatusErrors(t *testing.T) { t.Run("pkixNoCheck missing", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, false) opts := Options{ - CertChain: okChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: okChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) @@ -654,9 +603,10 @@ func TestCheckStatusErrors(t *testing.T) { t.Run("non-HTTP URI error", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ - CertChain: noHTTPChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: noHTTPChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { @@ -701,9 +651,10 @@ func TestCheckOCSPInvalidChain(t *testing.T) { t.Run("chain missing intermediate", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ - CertChain: missingIntermediateChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: missingIntermediateChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != missingIntermediateErr.Error() { @@ -717,9 +668,10 @@ func TestCheckOCSPInvalidChain(t *testing.T) { t.Run("chain out of order", func(t *testing.T) { client := testhelper.MockClient(misorderedIntermediateTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ - CertChain: misorderedIntermediateChain, - SigningTime: time.Now(), - HTTPClient: client, + CertChain: misorderedIntermediateChain, + CertChainPurpose: purpose.CodeSigning, + SigningTime: time.Now(), + HTTPClient: client, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != misorderedChainErr.Error() { diff --git a/revocation/result/results.go b/revocation/result/results.go index c7ecba51..09718cc3 100644 --- a/revocation/result/results.go +++ b/revocation/result/results.go @@ -16,23 +16,27 @@ package result import "strconv" -// Result is a type of enumerated value to help characterize errors. It can be -// OK, Unknown, or Revoked +// Result is a type of enumerated value to help characterize revocation result. +// It can be OK, Unknown, NonRevokable, or Revoked type Result int const ( // ResultUnknown is a Result that indicates that some error other than a - // revocation was encountered during the revocation check + // revocation was encountered during the revocation check. ResultUnknown Result = iota - // ResultOK is a Result that indicates that the revocation check resulted in no - // important errors + + // ResultOK is a Result that indicates that the revocation check resulted in + // no important errors. ResultOK - // ResultNonRevokable is a Result that indicates that the certificate cannot be - // checked for revocation. This may be a result of no OCSP servers being - // specified, the cert is a root certificate, or other related situations. + + // ResultNonRevokable is a Result that indicates that the certificate cannot + // be checked for revocation. This may be due to the absence of OCSP servers + // or CRL distribution points, or because the certificate is a root + // certificate. ResultNonRevokable + // ResultRevoked is a Result that indicates that at least one certificate was - // revoked when performing a revocation check on the certificate chain + // revoked when performing a revocation check on the certificate chain. ResultRevoked ) @@ -52,8 +56,45 @@ func (r Result) String() string { } } -// ServerResult encapsulates the result for a single server for a single -// certificate in the chain +// RevocationMethod defines the method used to check the revocation status of a +// certificate. +type RevocationMethod int + +const ( + // RevocationMethodUnknown is used for root certificates or when the method + // used to check the revocation status of a certificate is unknown. + RevocationMethodUnknown RevocationMethod = iota + + // RevocationMethodOCSP represents OCSP as the method used to check the + // revocation status of a certificate. + RevocationMethodOCSP + + // RevocationMethodCRL represents CRL as the method used to check the + // revocation status of a certificate. + RevocationMethodCRL + + // RevocationMethodOCSPFallbackCRL represents OCSP check with unknown error + // fallback to CRL as the method used to check the revocation status of a + // certificate. + RevocationMethodOCSPFallbackCRL +) + +// String provides a conversion from a Method to a string +func (m RevocationMethod) String() string { + switch m { + case RevocationMethodOCSP: + return "OCSP" + case RevocationMethodCRL: + return "CRL" + case RevocationMethodOCSPFallbackCRL: + return "OCSPFallbackCRL" + default: + return "Unknown" + } +} + +// ServerResult encapsulates the OCSP result for a single server or the CRL +// result for a single CRL URI for a certificate in the chain type ServerResult struct { // Result of revocation for this server (Unknown if there is an error which // prevents the retrieval of a valid status) @@ -67,6 +108,11 @@ type ServerResult struct { // Error is set if there is an error associated with the revocation check // to this server Error error + + // RevocationMethod is the method used to check the revocation status of the + // certificate, including RevocationMethodUnknown, RevocationMethodOCSP, + // RevocationMethodCRL + RevocationMethod RevocationMethod } // NewServerResult creates a ServerResult object from its individual parts: a @@ -83,21 +129,31 @@ func NewServerResult(result Result, server string, err error) *ServerResult { // chain as well as the results from individual servers associated with this // certificate type CertRevocationResult struct { - // Result of revocation for a specific cert in the chain - // - // If there are multiple ServerResults, this is because no responses were - // able to be retrieved, leaving each ServerResult with a Result of Unknown. - // Thus, in the case of more than one ServerResult, this will be ResultUnknown + // Result of revocation for a specific certificate in the chain. Result Result - // An array of results for each server associated with the certificate. - // The length will be either 1 or the number of OCSPServers for the cert. + // ServerResults is an array of results for each server associated with the + // certificate. // - // If the length is 1, then a valid status was able to be retrieved. Only + // When RevocationMethod is MethodOCSP, the length will be + // either 1 or the number of OCSPServers for the certificate. + // If the length is 1, then a valid status was retrieved. Only // this server result is contained. Any errors for other servers are // discarded in favor of this valid response. - // // Otherwise, every server specified had some error that prevented the - // status from being retrieved. These are all contained here for evaluation + // status from being retrieved. These are all contained here for evaluation. + // + // When RevocationMethod is MethodCRL, the length will be the number of + // CRL distribution points' URIs checked. If the result is Revoked, or + // there is an error, the length will be 1. + // + // When RevocationMethod is MethodOCSPFallbackCRL, the length + // will be the sum of the previous two cases. The CRL result will be + // appended after the OCSP results. ServerResults []*ServerResult + + // RevocationMethod is the method used to check the revocation status of the + // certificate, including RevocationMethodUnknown, RevocationMethodOCSP, + // RevocationMethodCRL and RevocationMethodOCSPFallbackCRL + RevocationMethod RevocationMethod } diff --git a/revocation/result/results_test.go b/revocation/result/results_test.go index 1c5a503a..75b42ae3 100644 --- a/revocation/result/results_test.go +++ b/revocation/result/results_test.go @@ -46,6 +46,27 @@ func TestResultString(t *testing.T) { }) } +func TestMethodString(t *testing.T) { + tests := []struct { + method RevocationMethod + expected string + }{ + {RevocationMethodOCSP, "OCSP"}, + {RevocationMethodCRL, "CRL"}, + {RevocationMethodOCSPFallbackCRL, "OCSPFallbackCRL"}, + {RevocationMethod(999), "Unknown"}, // Test for default case + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + result := tt.method.String() + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} + func TestNewServerResult(t *testing.T) { expectedR := &ServerResult{ Result: ResultNonRevokable, diff --git a/revocation/revocation.go b/revocation/revocation.go index 25ac11da..a20c63c1 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -21,9 +21,12 @@ import ( "errors" "fmt" "net/http" + "sync" "time" - "github.com/notaryproject/notation-core-go/revocation/ocsp" + "github.com/notaryproject/notation-core-go/revocation/internal/crl" + "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" + "github.com/notaryproject/notation-core-go/revocation/internal/x509util" "github.com/notaryproject/notation-core-go/revocation/purpose" "github.com/notaryproject/notation-core-go/revocation/result" ) @@ -34,8 +37,9 @@ import ( // To perform revocation check, use [Validator]. type Revocation interface { // Validate checks the revocation status for a certificate chain using OCSP - // and returns an array of CertRevocationResults that contain the results - // and any errors that are encountered during the process + // and CRL if OCSP is not available. It returns an array of + // CertRevocationResults that contain the results and any errors that are + // encountered during the process Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) } @@ -64,7 +68,8 @@ type Validator interface { // revocation is an internal struct used for revocation checking type revocation struct { - httpClient *http.Client + ocspHTTPClient *http.Client + crlHTTPClient *http.Client certChainPurpose purpose.Purpose } @@ -77,7 +82,8 @@ func New(httpClient *http.Client) (Revocation, error) { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } return &revocation{ - httpClient: httpClient, + ocspHTTPClient: httpClient, + crlHTTPClient: httpClient, certChainPurpose: purpose.CodeSigning, }, nil } @@ -89,6 +95,11 @@ type Options struct { // OPTIONAL. OCSPHTTPClient *http.Client + // CRLHTTPClient is the HTTP client for CRL request. If not provided, + // a default *http.Client with timeout of 5 seconds will be used. + // OPTIONAL. + CRLHTTPClient *http.Client + // CertChainPurpose is the purpose of the certificate chain. Supported // values are CodeSigning and Timestamping. Default value is CodeSigning. // OPTIONAL. @@ -101,6 +112,10 @@ func NewWithOptions(opts Options) (Validator, error) { opts.OCSPHTTPClient = &http.Client{Timeout: 2 * time.Second} } + if opts.CRLHTTPClient == nil { + opts.CRLHTTPClient = &http.Client{Timeout: 5 * time.Second} + } + switch opts.CertChainPurpose { case purpose.CodeSigning, purpose.Timestamping: default: @@ -108,17 +123,22 @@ func NewWithOptions(opts Options) (Validator, error) { } return &revocation{ - httpClient: opts.OCSPHTTPClient, + ocspHTTPClient: opts.OCSPHTTPClient, + crlHTTPClient: opts.CRLHTTPClient, certChainPurpose: opts.CertChainPurpose, }, nil } // Validate checks the revocation status for a certificate chain using OCSP and -// returns an array of CertRevocationResults that contain the results and any -// errors that are encountered during the process +// CRL if OCSP is not available. It returns an array of CertRevocationResults +// that contain the results and any errors that are encountered during the +// process. // -// TODO: add CRL support -// https://github.com/notaryproject/notation-core-go/issues/125 +// This function tries OCSP and falls back to CRL when: +// - OCSP is not supported by the certificate +// - OCSP returns an unknown status +// +// NOTE: The certificate chain is expected to be in the order of leaf to root. func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { return r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: certChain, @@ -126,24 +146,114 @@ func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Ti }) } -// ValidateContext checks the revocation status for a certificate chain using -// OCSP and returns an array of CertRevocationResults that contain the results -// and any errors that are encountered during the process +// ValidateContext checks the revocation status for a certificate chain using OCSP and +// CRL if OCSP is not available. It returns an array of CertRevocationResults +// that contain the results and any errors that are encountered during the +// process. +// +// This function tries OCSP and falls back to CRL when: +// - OCSP is not supported by the certificate +// - OCSP returns an unknown status // -// TODO: add CRL support -// https://github.com/notaryproject/notation-core-go/issues/125 +// NOTE: The certificate chain is expected to be in the order of leaf to root. func (r *revocation) ValidateContext(ctx context.Context, validateContextOpts ValidateContextOptions) ([]*result.CertRevocationResult, error) { + // validate certificate chain if len(validateContextOpts.CertChain) == 0 { return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} } + certChain := validateContextOpts.CertChain + if err := x509util.ValidateChain(certChain, r.certChainPurpose); err != nil { + return nil, err + } - return ocsp.CheckStatus(ocsp.Options{ - CertChain: validateContextOpts.CertChain, - CertChainPurpose: r.certChainPurpose, - SigningTime: validateContextOpts.AuthenticSigningTime, - HTTPClient: r.httpClient, - }) + ocspOpts := ocsp.CertCheckStatusOptions{ + HTTPClient: r.ocspHTTPClient, + SigningTime: validateContextOpts.AuthenticSigningTime, + } + crlOpts := crl.CertCheckStatusOptions{ + HTTPClient: r.crlHTTPClient, + SigningTime: validateContextOpts.AuthenticSigningTime, + } + + // panicChain is used to store the panic in goroutine and handle it + panicChan := make(chan any, len(certChain)) + defer close(panicChan) + + certResults := make([]*result.CertRevocationResult, len(certChain)) + var wg sync.WaitGroup + for i, cert := range certChain[:len(certChain)-1] { + switch { + case ocsp.Supported(cert): + // do OCSP check for the certificate + wg.Add(1) + + go func(i int, cert *x509.Certificate) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + // catch panic and send it to panicChan to avoid + // losing the panic + panicChan <- r + } + }() + + ocspResult := ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) + if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.Supported(cert) { + // try CRL check if OCSP serverResult is unknown + serverResult := crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts) + // append CRL result to OCSP result + serverResult.ServerResults = append(ocspResult.ServerResults, serverResult.ServerResults...) + serverResult.RevocationMethod = result.RevocationMethodOCSPFallbackCRL + certResults[i] = serverResult + } else { + certResults[i] = ocspResult + } + }(i, cert) + case crl.Supported(cert): + // do CRL check for the certificate + wg.Add(1) + + go func(i int, cert *x509.Certificate) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + // catch panic and send it to panicChan to avoid + // losing the panic + panicChan <- r + } + }() + + certResults[i] = crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts) + }(i, cert) + default: + certResults[i] = &result.CertRevocationResult{ + Result: result.ResultNonRevokable, + ServerResults: []*result.ServerResult{{ + Result: result.ResultNonRevokable, + RevocationMethod: result.RevocationMethodUnknown, + }}, + RevocationMethod: result.RevocationMethodUnknown, + } + } + } + + // Last is root cert, which will never be revoked by OCSP or CRL + certResults[len(certChain)-1] = &result.CertRevocationResult{ + Result: result.ResultNonRevokable, + ServerResults: []*result.ServerResult{{ + Result: result.ResultNonRevokable, + RevocationMethod: result.RevocationMethodUnknown, + }}, + RevocationMethod: result.RevocationMethodUnknown, + } + wg.Wait() + + // handle panic + select { + case p := <-panicChan: + panic(p) + default: + } - // TODO: add CRL support - // https://github.com/notaryproject/notation-core-go/issues/125 + return certResults, nil } diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index f663880c..2ac8b4c9 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -14,15 +14,21 @@ package revocation import ( + "bytes" "context" + "crypto/rand" "crypto/x509" "errors" "fmt" + "io" + "math/big" "net/http" + "strconv" + "strings" "testing" "time" - revocationocsp "github.com/notaryproject/notation-core-go/revocation/ocsp" + revocationocsp "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" "github.com/notaryproject/notation-core-go/revocation/purpose" "github.com/notaryproject/notation-core-go/revocation/result" "github.com/notaryproject/notation-core-go/testhelper" @@ -60,6 +66,9 @@ func validateEquivalentCertResults(certResults, expectedCertResults []*result.Ce t.Errorf("Expected certResults[%d].ServerResults[%d].Error to be %v, but got %v", i, j, expectedCertResults[i].ServerResults[j].Error, serverResult.Error) } } + if certResult.RevocationMethod != expectedCertResults[i].RevocationMethod { + t.Errorf("Expected certResults[%d].RevocationMethod to be %d, but got %d", i, expectedCertResults[i].RevocationMethod, certResult.RevocationMethod) + } } } @@ -69,6 +78,7 @@ func getOKCertResult(server string) *result.CertRevocationResult { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultOK, server, nil), }, + RevocationMethod: result.RevocationMethodOCSP, } } @@ -96,8 +106,8 @@ func TestNew(t *testing.T) { revR, ok := r.(*revocation) if !ok { t.Error("Expected New to create an object matching the internal revocation struct") - } else if revR.httpClient != client { - t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.httpClient) + } else if revR.ocspHTTPClient != client { + t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.ocspHTTPClient) } } @@ -161,6 +171,7 @@ func TestCheckRevocationStatusForSingleCert(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[0].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getRootCertResult(), } @@ -183,6 +194,7 @@ func TestCheckRevocationStatusForSingleCert(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[0].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getRootCertResult(), } @@ -305,6 +317,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -332,6 +345,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -359,6 +373,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), { @@ -366,6 +381,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getRootCertResult(), } @@ -415,6 +431,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -442,6 +459,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -475,6 +493,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -493,6 +512,18 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { revokableChain[i].NotBefore = zeroTime } + t.Run("invalid revocation purpose", func(t *testing.T) { + revocationClient := &revocation{ + ocspHTTPClient: &http.Client{Timeout: 5 * time.Second}, + certChainPurpose: -1, + } + + _, err := revocationClient.Validate(revokableChain, time.Now()) + if err == nil { + t.Error("Expected Validate to fail with an error, but it succeeded") + } + }) + t.Run("empty chain", func(t *testing.T) { r, err := NewWithOptions(Options{ OCSPHTTPClient: &http.Client{Timeout: 5 * time.Second}, @@ -564,6 +595,7 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -596,6 +628,7 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -628,6 +661,7 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), { @@ -635,6 +669,7 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getRootCertResult(), } @@ -694,6 +729,7 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -726,6 +762,7 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -762,6 +799,7 @@ func TestCheckRevocationStatusForTimestampChain(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), @@ -856,12 +894,14 @@ func TestCheckRevocationErrors(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, okChain[0].OCSPServer[0], revocationocsp.TimeoutError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, okChain[1].OCSPServer[0], revocationocsp.TimeoutError{}), }, + RevocationMethod: result.RevocationMethodOCSP, }, getRootCertResult(), } @@ -884,6 +924,7 @@ func TestCheckRevocationErrors(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, expiredChain[0].OCSPServer[0], expiredRespErr), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(expiredChain[1].OCSPServer[0]), getRootCertResult(), @@ -926,6 +967,7 @@ func TestCheckRevocationErrors(t *testing.T) { ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, noHTTPChain[0].OCSPServer[0], noHTTPErr), }, + RevocationMethod: result.RevocationMethodOCSP, }, getOKCertResult(noHTTPChain[1].OCSPServer[0]), getRootCertResult(), @@ -989,9 +1031,306 @@ func TestCheckRevocationInvalidChain(t *testing.T) { }) } +func TestCRL(t *testing.T) { + t.Run("CRL check valid", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(3, false, true) + + revocationClient, err := NewWithOptions(Options{ + CRLHTTPClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: false, + }, + }, + OCSPHTTPClient: &http.Client{}, + CertChainPurpose: purpose.CodeSigning, + }) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + + certResults, err := revocationClient.ValidateContext(context.Background(), ValidateContextOptions{ + CertChain: []*x509.Certificate{chain[0].Cert, chain[1].Cert, chain[2].Cert}, + AuthenticSigningTime: time.Now(), + }) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + + expectedCertResults := []*result.CertRevocationResult{ + { + Result: result.ResultOK, + ServerResults: []*result.ServerResult{{ + Result: result.ResultOK, + Server: "http://example.com/chain_crl/0", + }}, + RevocationMethod: result.RevocationMethodCRL, + }, + { + Result: result.ResultOK, + ServerResults: []*result.ServerResult{{ + Result: result.ResultOK, + Server: "http://example.com/chain_crl/1", + }}, + RevocationMethod: result.RevocationMethodCRL, + }, + getRootCertResult(), + } + + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + + t.Run("CRL check with revoked status", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(3, false, true) + + revocationClient, err := NewWithOptions(Options{ + CRLHTTPClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: true, + }, + }, + OCSPHTTPClient: &http.Client{}, + CertChainPurpose: purpose.CodeSigning, + }) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + + certResults, err := revocationClient.ValidateContext(context.Background(), ValidateContextOptions{ + CertChain: []*x509.Certificate{ + chain[0].Cert, // leaf + chain[1].Cert, // intermediate + chain[2].Cert, // root + }, + AuthenticSigningTime: time.Now(), + }) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + + expectedCertResults := []*result.CertRevocationResult{ + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + { + Result: result.ResultRevoked, + Server: "http://example.com/chain_crl/0", + }, + }, + RevocationMethod: result.RevocationMethodCRL, + }, + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + { + Result: result.ResultRevoked, + Server: "http://example.com/chain_crl/1", + }, + }, + RevocationMethod: result.RevocationMethodCRL, + }, + getRootCertResult(), + } + + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + + t.Run("OCSP fallback to CRL", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(3, true, true) + + revocationClient, err := NewWithOptions(Options{ + CRLHTTPClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: true, + FailOCSP: true, + }, + }, + OCSPHTTPClient: &http.Client{}, + CertChainPurpose: purpose.CodeSigning, + }) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + + certResults, err := revocationClient.ValidateContext(context.Background(), ValidateContextOptions{ + CertChain: []*x509.Certificate{ + chain[0].Cert, // leaf + chain[1].Cert, // intermediate + chain[2].Cert, // root + }, + AuthenticSigningTime: time.Now(), + }) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + + expectedCertResults := []*result.CertRevocationResult{ + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + { + Result: result.ResultUnknown, + Server: "http://example.com/chain_ocsp/0", + Error: errors.New("failed to retrieve OCSP: response had status code 500"), + RevocationMethod: result.RevocationMethodOCSP, + }, + { + Result: result.ResultRevoked, + Server: "http://example.com/chain_crl/0", + RevocationMethod: result.RevocationMethodCRL, + }, + }, + RevocationMethod: result.RevocationMethodOCSPFallbackCRL, + }, + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + { + Result: result.ResultUnknown, + Server: "http://example.com/chain_ocsp/1", + Error: errors.New("failed to retrieve OCSP: response had status code 500"), + RevocationMethod: result.RevocationMethodOCSPFallbackCRL, + }, + { + Result: result.ResultRevoked, + Server: "http://example.com/chain_crl/1", + RevocationMethod: result.RevocationMethodCRL, + }, + }, + RevocationMethod: result.RevocationMethodOCSPFallbackCRL, + }, + getRootCertResult(), + } + + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) +} + +func TestPanicHandling(t *testing.T) { + t.Run("panic in OCSP", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(2, true, false) + client := &http.Client{ + Transport: panicTransport{}, + } + + r, err := NewWithOptions(Options{ + OCSPHTTPClient: client, + CRLHTTPClient: client, + CertChainPurpose: purpose.CodeSigning, + }) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic, but got nil") + } + }() + _, _ = r.ValidateContext(context.Background(), ValidateContextOptions{ + CertChain: []*x509.Certificate{chain[0].Cert, chain[1].Cert}, + AuthenticSigningTime: time.Now(), + }) + + }) + + t.Run("panic in CRL", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + client := &http.Client{ + Transport: panicTransport{}, + } + + r, err := NewWithOptions(Options{ + OCSPHTTPClient: client, + CRLHTTPClient: client, + CertChainPurpose: purpose.CodeSigning, + }) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic, but got nil") + } + }() + _, _ = r.ValidateContext(context.Background(), ValidateContextOptions{ + CertChain: []*x509.Certificate{chain[0].Cert, chain[1].Cert}, + AuthenticSigningTime: time.Now(), + }) + }) +} + +type crlRoundTripper struct { + CertChain []testhelper.RSACertTuple + Revoked bool + FailOCSP bool +} + +func (rt *crlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + // e.g. ocsp URL: http://example.com/chain_ocsp/0 + // e.g. crl URL: http://example.com/chain_crl/0 + parts := strings.Split(req.URL.Path, "/") + + isOCSP := parts[len(parts)-2] == "chain_ocsp" + // fail OCSP + if rt.FailOCSP && isOCSP { + return nil, errors.New("OCSP failed") + } + + // choose the cert suffix based on suffix of request url + // e.g. http://example.com/chain_crl/0 -> 0 + i, err := strconv.Atoi(parts[len(parts)-1]) + if err != nil { + return nil, err + } + if i >= len(rt.CertChain) { + return nil, errors.New("invalid index") + } + + cert := rt.CertChain[i].Cert + crl := &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + } + + if rt.Revoked { + crl.RevokedCertificateEntries = []x509.RevocationListEntry{ + { + SerialNumber: cert.SerialNumber, + RevocationTime: time.Now().Add(-time.Hour), + }, + } + } + + issuerCert := rt.CertChain[i+1].Cert + issuerKey := rt.CertChain[i+1].PrivateKey + crlBytes, err := x509.CreateRevocationList(rand.Reader, crl, issuerCert, issuerKey) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(crlBytes)), + }, nil +} + +type panicTransport struct{} + +func (t panicTransport) RoundTrip(req *http.Request) (*http.Response, error) { + panic("panic") +} + func TestValidateContext(t *testing.T) { r, err := NewWithOptions(Options{ - OCSPHTTPClient: &http.Client{}, + OCSPHTTPClient: &http.Client{}, + CertChainPurpose: purpose.CodeSigning, }) if err != nil { t.Fatal(err) From 09c668a59c78b9933bbde5124b92e412990f6bac Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Sat, 14 Sep 2024 09:32:36 +0000 Subject: [PATCH 075/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 8 ++ revocation/crl/fetcher/fetcher.go | 13 ++- revocation/internal/crl/crl.go | 89 ++++++++----------- revocation/internal/crl/crl_test.go | 131 ++++++++-------------------- revocation/revocation.go | 22 +++++ 5 files changed, 116 insertions(+), 147 deletions(-) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index f14b947c..ba728780 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -29,7 +29,14 @@ const ( // CRLMetadata stores the URL of the CRL type CRLMetadata struct { + // URL stores the URL of the CRL URL string `json:"url"` + + // NextUpdate stores the next update time of the CRL + // + // This field extracts the `NextUpdate` field from the CRL extensions, which + // is an optional field in the CRL, so it may be empty. + NextUpdate time.Time `json:"nextUpdate"` } // Metadata stores the metadata infomation of the CRL @@ -56,6 +63,7 @@ type Bundle struct { Metadata Metadata } +// Validate checks if the bundle is valid func (b *Bundle) Validate() error { if b.BaseCRL == nil { return errors.New("base CRL is missing") diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher/fetcher.go index 2caf3e88..9cfe77bf 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -29,12 +29,19 @@ import ( ) // maxCRLSize is the maximum size of CRL in bytes -const maxCRLSize = 64 * 1024 * 1024 // 64 MiB +// +// CRL examples: https://chasersystems.com/blog/an-analysis-of-certificate-revocation-list-sizes/ +const maxCRLSize = 32 * 1024 * 1024 // 32 MiB // Fetcher is an interface that specifies methods used for fetching CRL // from the given URL type Fetcher interface { + // Fetch retrieves the CRL from the given Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) + + // Download downloads the CRL from the given URL and saves it to the + // cache. + Download(ctx context.Context, crlURL string) (bundle *cache.Bundle, err error) } type cachedFetcher struct { @@ -44,7 +51,6 @@ type cachedFetcher struct { // NewCachedFetcher creates a new Fetcher with the given HTTP client and cache client // - if httpClient is nil, http.DefaultClient will be used -// - if cacheClient is nil, no cache will be used func NewCachedFetcher(httpClient *http.Client, cacheClient cache.Cache) (Fetcher, error) { if httpClient == nil { httpClient = http.DefaultClient @@ -147,7 +153,8 @@ func download(ctx context.Context, crlURL string, client *http.Client) (bundle * BaseCRL: crl, Metadata: cache.Metadata{ BaseCRL: cache.CRLMetadata{ - URL: crlURL, + URL: crlURL, + NextUpdate: crl.NextUpdate, }, CreatedAt: time.Now(), }, diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index 48a5930a..56edb2c3 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -21,11 +21,11 @@ import ( "encoding/asn1" "errors" "fmt" - "io" "net/http" - "net/url" "time" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" + "github.com/notaryproject/notation-core-go/revocation/crl/fetcher" "github.com/notaryproject/notation-core-go/revocation/result" ) @@ -43,11 +43,6 @@ var ( oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} ) -// maxCRLSize is the maximum size of CRL in bytes -// -// CRL examples: https://chasersystems.com/blog/an-analysis-of-certificate-revocation-list-sizes/ -const maxCRLSize = 32 * 1024 * 1024 // 32 MiB - // CertCheckStatusOptions specifies values that are needed to check CRL type CertCheckStatusOptions struct { // HTTPClient is the HTTP client used to download CRL @@ -56,6 +51,9 @@ type CertCheckStatusOptions struct { // SigningTime is used to compare with the invalidity date during revocation // check SigningTime time.Time + + // cacheClient is the cache client used to store the CRL + CacheClient cache.Cache } // CertCheckStatus checks the revocation status of a certificate using CRL @@ -91,19 +89,47 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C lastErr error crlURL string ) + cachedFetcher, err := fetcher.NewCachedFetcher(opts.HTTPClient, opts.CacheClient) + if err != nil { + return &result.CertRevocationResult{ + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{{ + Result: result.ResultUnknown, + Error: err, + RevocationMethod: result.RevocationMethodCRL, + }}, + RevocationMethod: result.RevocationMethodCRL, + } + } + for _, crlURL = range cert.CRLDistributionPoints { - baseCRL, err := download(ctx, crlURL, opts.HTTPClient) + bundle, fromCache, err := cachedFetcher.Fetch(ctx, crlURL) if err != nil { lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err) break } - if err = validate(baseCRL, issuer); err != nil { - lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err) - break + if err = validate(bundle.BaseCRL, issuer); err != nil { + if fromCache { + // the CRL may be stale, try to download again + bundle, err = cachedFetcher.Download(ctx, crlURL) + if err != nil { + lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err) + break + } + + if err = validate(bundle.BaseCRL, issuer); err != nil { + lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err) + break + } + } else { + // the CRL is fresh, but it is invalid + lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err) + break + } } - crlResult, err := checkRevocation(cert, baseCRL, opts.SigningTime, crlURL) + crlResult, err := checkRevocation(cert, bundle.BaseCRL, opts.SigningTime, crlURL) if err != nil { lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err) break @@ -247,42 +273,3 @@ func parseEntryExtensions(entry x509.RevocationListEntry) (entryExtensions, erro return extensions, nil } - -func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { - // validate URL - parsedURL, err := url.Parse(crlURL) - if err != nil { - return nil, fmt.Errorf("invalid CRL URL: %w", err) - } - if parsedURL.Scheme != "http" { - return nil, fmt.Errorf("unsupported CRL endpoint: %s. Only urls with HTTP scheme is supported", crlURL) - } - - // download CRL - req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to create CRL request %q: %w", crlURL, err) - } - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("request failed for %q: %w", crlURL, err) - } - defer resp.Body.Close() - - // check response - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("%s %q: failed to download with status code: %d", resp.Request.Method, resp.Request.URL, resp.StatusCode) - } - - // read with size limit - limitedReader := io.LimitReader(resp.Body, maxCRLSize) - data, err := io.ReadAll(limitedReader) - if err != nil { - return nil, fmt.Errorf("failed to read CRL response from %q: %w", resp.Request.URL, err) - } - if len(data) == maxCRLSize { - return nil, fmt.Errorf("%s %q: CRL size reached the %d MiB size limit", resp.Request.Method, resp.Request.URL, maxCRLSize/1024/1024) - } - - return x509.ParseRevocationList(data) -} diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 2129fb79..e7fd1a82 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -28,12 +28,13 @@ import ( "testing" "time" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/result" "github.com/notaryproject/notation-core-go/testhelper" ) func TestCertCheckStatus(t *testing.T) { - t.Run("certtificate does not have CRLDistributionPoints", func(t *testing.T) { + t.Run("certificate does not have CRLDistributionPoints", func(t *testing.T) { cert := &x509.Certificate{} r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{}) if r.Result != result.ResultNonRevokable { @@ -42,6 +43,11 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("download error", func(t *testing.T) { + memoryCache, err := cache.NewMemoryCache() + if err != nil { + t.Fatal(err) + } + cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, } @@ -49,6 +55,7 @@ func TestCertCheckStatus(t *testing.T) { HTTPClient: &http.Client{ Transport: errorRoundTripperMock{}, }, + CacheClient: memoryCache, }) if r.ServerResults[0].Error == nil { t.Fatal("expected error") @@ -56,6 +63,11 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL validate failed", func(t *testing.T) { + memoryCache, err := cache.NewMemoryCache() + if err != nil { + t.Fatal(err) + } + cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, } @@ -63,6 +75,7 @@ func TestCertCheckStatus(t *testing.T) { HTTPClient: &http.Client{ Transport: expiredCRLRoundTripperMock{}, }, + CacheClient: memoryCache, }) if r.ServerResults[0].Error == nil { t.Fatal("expected error") @@ -75,6 +88,11 @@ func TestCertCheckStatus(t *testing.T) { issuerKey := chain[1].PrivateKey t.Run("revoked", func(t *testing.T) { + memoryCache, err := cache.NewMemoryCache() + if err != nil { + t.Fatal(err) + } + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(20240720), @@ -93,6 +111,7 @@ func TestCertCheckStatus(t *testing.T) { HTTPClient: &http.Client{ Transport: expectedRoundTripperMock{Body: crlBytes}, }, + CacheClient: memoryCache, }) if r.Result != result.ResultRevoked { t.Fatalf("expected revoked, got %s", r.Result) @@ -100,6 +119,11 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("unknown critical extension", func(t *testing.T) { + memoryCache, err := cache.NewMemoryCache() + if err != nil { + t.Fatal(err) + } + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(20240720), @@ -124,6 +148,7 @@ func TestCertCheckStatus(t *testing.T) { HTTPClient: &http.Client{ Transport: expectedRoundTripperMock{Body: crlBytes}, }, + CacheClient: memoryCache, }) if r.ServerResults[0].Error == nil { t.Fatal("expected error") @@ -131,6 +156,11 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("Not revoked", func(t *testing.T) { + memoryCache, err := cache.NewMemoryCache() + if err != nil { + t.Fatal(err) + } + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(20240720), @@ -143,6 +173,7 @@ func TestCertCheckStatus(t *testing.T) { HTTPClient: &http.Client{ Transport: expectedRoundTripperMock{Body: crlBytes}, }, + CacheClient: memoryCache, }) if r.Result != result.ResultOK { t.Fatalf("expected OK, got %s", r.Result) @@ -150,6 +181,11 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL with delta CRL is not checked", func(t *testing.T) { + memoryCache, err := cache.NewMemoryCache() + if err != nil { + t.Fatal(err) + } + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(20240720), @@ -168,6 +204,7 @@ func TestCertCheckStatus(t *testing.T) { HTTPClient: &http.Client{ Transport: expectedRoundTripperMock{Body: crlBytes}, }, + CacheClient: memoryCache, }) if !errors.Is(r.ServerResults[0].Error, ErrDeltaCRLNotSupported) { t.Fatal("expected ErrDeltaCRLNotChecked") @@ -535,66 +572,6 @@ func marshalGeneralizedTimeToBytes(t time.Time) ([]byte, error) { return asn1.Marshal(t) } -func TestDownload(t *testing.T) { - t.Run("parse url error", func(t *testing.T) { - _, err := download(context.Background(), ":", http.DefaultClient) - if err == nil { - t.Fatal("expected error") - } - }) - t.Run("https download", func(t *testing.T) { - _, err := download(context.Background(), "https://example.com", http.DefaultClient) - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("http.NewRequestWithContext error", func(t *testing.T) { - var ctx context.Context = nil - _, err := download(ctx, "http://example.com", &http.Client{}) - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("client.Do error", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: errorRoundTripperMock{}, - }) - - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("status code is not 2xx", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: serverErrorRoundTripperMock{}, - }) - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("readAll error", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: readFailedRoundTripperMock{}, - }) - if err == nil { - t.Fatal("expected error") - } - }) - - t.Run("exceed the size limit", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, - }) - if err == nil { - t.Fatal("expected error") - } - }) -} - func TestSupported(t *testing.T) { t.Run("supported", func(t *testing.T) { cert := &x509.Certificate{ @@ -619,28 +596,6 @@ func (rt errorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, er return nil, fmt.Errorf("error") } -type serverErrorRoundTripperMock struct{} - -func (rt serverErrorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { - return &http.Response{ - Request: req, - StatusCode: http.StatusInternalServerError, - }, nil -} - -type readFailedRoundTripperMock struct{} - -func (rt readFailedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: errorReaderMock{}, - Request: &http.Request{ - Method: http.MethodGet, - URL: req.URL, - }, - }, nil -} - type expiredCRLRoundTripperMock struct{} func (rt expiredCRLRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { @@ -662,16 +617,6 @@ func (rt expiredCRLRoundTripperMock) RoundTrip(req *http.Request) (*http.Respons }, nil } -type errorReaderMock struct{} - -func (r errorReaderMock) Read(p []byte) (n int, err error) { - return 0, fmt.Errorf("error") -} - -func (r errorReaderMock) Close() error { - return nil -} - type expectedRoundTripperMock struct { Body []byte } diff --git a/revocation/revocation.go b/revocation/revocation.go index a20c63c1..b04783ad 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -24,6 +24,7 @@ import ( "sync" "time" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/internal/crl" "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" "github.com/notaryproject/notation-core-go/revocation/internal/x509util" @@ -71,6 +72,7 @@ type revocation struct { ocspHTTPClient *http.Client crlHTTPClient *http.Client certChainPurpose purpose.Purpose + crlCache cache.Cache } // New constructs a revocation object for code signing certificate chain. @@ -81,10 +83,16 @@ func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } + memoryCache, err := cache.NewMemoryCache() + if err != nil { + return nil, fmt.Errorf("failed to create memory cache: %v", err) + } + return &revocation{ ocspHTTPClient: httpClient, crlHTTPClient: httpClient, certChainPurpose: purpose.CodeSigning, + crlCache: memoryCache, }, nil } @@ -104,6 +112,10 @@ type Options struct { // values are CodeSigning and Timestamping. Default value is CodeSigning. // OPTIONAL. CertChainPurpose purpose.Purpose + + // CRLCache is the cache client used to store the CRL. if not provided, + // a default in-memory cache will be used. + CRLCache cache.Cache } // NewWithOptions constructs a Validator with the specified options @@ -122,10 +134,19 @@ func NewWithOptions(opts Options) (Validator, error) { return nil, fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose) } + if opts.CRLCache == nil { + memoryCache, err := cache.NewMemoryCache() + if err != nil { + return nil, fmt.Errorf("failed to create memory cache: %v", err) + } + opts.CRLCache = memoryCache + } + return &revocation{ ocspHTTPClient: opts.OCSPHTTPClient, crlHTTPClient: opts.CRLHTTPClient, certChainPurpose: opts.CertChainPurpose, + crlCache: opts.CRLCache, }, nil } @@ -173,6 +194,7 @@ func (r *revocation) ValidateContext(ctx context.Context, validateContextOpts Va crlOpts := crl.CertCheckStatusOptions{ HTTPClient: r.crlHTTPClient, SigningTime: validateContextOpts.AuthenticSigningTime, + CacheClient: r.crlCache, } // panicChain is used to store the panic in goroutine and handle it From a88cd4e81dae5d5efa21a672c1d2c46a50a7dc3a Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Sat, 14 Sep 2024 09:35:33 +0000 Subject: [PATCH 076/110] fix: update Signed-off-by: Junjie Gao --- revocation/ocsp/{error.go => errors.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename revocation/ocsp/{error.go => errors.go} (100%) diff --git a/revocation/ocsp/error.go b/revocation/ocsp/errors.go similarity index 100% rename from revocation/ocsp/error.go rename to revocation/ocsp/errors.go From 0d64aed46922e65d90f5ceca9322f9146e61e4bf Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 06:14:17 +0000 Subject: [PATCH 077/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher/fetcher.go | 2 +- revocation/internal/crl/crl.go | 16 ++-- revocation/internal/crl/crl_test.go | 101 ++++++++++++++++++++++++ revocation/ocsp/{error.go => errors.go} | 0 4 files changed, 111 insertions(+), 8 deletions(-) rename revocation/ocsp/{error.go => errors.go} (100%) diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher/fetcher.go index 9cfe77bf..4c9cd55c 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -36,7 +36,7 @@ const maxCRLSize = 32 * 1024 * 1024 // 32 MiB // Fetcher is an interface that specifies methods used for fetching CRL // from the given URL type Fetcher interface { - // Fetch retrieves the CRL from the given + // Fetch retrieves the CRL from the given URL. Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) // Download downloads the CRL from the given URL and saves it to the diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index 56edb2c3..3b78f3ab 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -71,24 +71,19 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{{ RevocationMethod: result.RevocationMethodCRL, + Error: errors.New("CRL is not supported"), Result: result.ResultNonRevokable, }}, RevocationMethod: result.RevocationMethodCRL, } } - // The CRLDistributionPoints contains the URIs of all the CRL distribution - // points. Since it does not distinguish the reason field, it needs to check - // all the URIs to avoid missing any partial CRLs. - // - // For the majority of the certificates, there is only one CRL distribution - // point with one CRL URI, which will be cached, so checking all the URIs is - // not a performance issue. var ( serverResults = make([]*result.ServerResult, 0, len(cert.CRLDistributionPoints)) lastErr error crlURL string ) + cachedFetcher, err := fetcher.NewCachedFetcher(opts.HTTPClient, opts.CacheClient) if err != nil { return &result.CertRevocationResult{ @@ -102,6 +97,13 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C } } + // The CRLDistributionPoints contains the URIs of all the CRL distribution + // points. Since it does not distinguish the reason field, it needs to check + // all the URIs to avoid missing any partial CRLs. + // + // For the majority of the certificates, there is only one CRL distribution + // point with one CRL URI, which will be cached, so checking all the URIs is + // not a performance issue. for _, crlURL = range cert.CRLDistributionPoints { bundle, fromCache, err := cachedFetcher.Fetch(ctx, crlURL) if err != nil { diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index e7fd1a82..b1fe4613 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -25,6 +25,7 @@ import ( "io" "math/big" "net/http" + "strings" "testing" "time" @@ -210,6 +211,106 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal("expected ErrDeltaCRLNotChecked") } }) + + t.Run("CRL cache is nil", func(t *testing.T) { + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: expectedRoundTripperMock{Body: []byte{}}, + }, + }) + if r.ServerResults[0].Error.Error() != "cache client is nil" { + t.Fatal("expected error") + } + }) + + memoryCache, err := cache.NewMemoryCache() + if err != nil { + t.Fatal(err) + } + + // create a stale CRL + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(-time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatal(err) + } + bundle := &cache.Bundle{ + BaseCRL: crl, + Metadata: cache.Metadata{ + BaseCRL: cache.CRLMetadata{ + URL: "http://example.com", + NextUpdate: crl.NextUpdate, + }, + CreatedAt: time.Now(), + }, + } + chain[0].Cert.CRLDistributionPoints = []string{"http://example.com"} + + t.Run("invalid stale CRL cache, and re-download failed", func(t *testing.T) { + // save to cache + if err := memoryCache.Set(context.Background(), "http://example.com", bundle); err != nil { + t.Fatal(err) + } + + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: errorRoundTripperMock{}, + }, + CacheClient: memoryCache, + }) + if !strings.HasPrefix(r.ServerResults[0].Error.Error(), "failed to download CRL from") { + t.Fatalf("unexpected error, got %v", r.ServerResults[0].Error) + } + }) + + t.Run("invalid stale CRL cache, re-download and still validate failed", func(t *testing.T) { + // save to cache + if err := memoryCache.Set(context.Background(), "http://example.com", bundle); err != nil { + t.Fatal(err) + } + + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: expectedRoundTripperMock{Body: crlBytes}, + }, + CacheClient: memoryCache, + }) + if !strings.HasPrefix(r.ServerResults[0].Error.Error(), "failed to validate CRL from") { + t.Fatalf("unexpected error, got %v", r.ServerResults[0].Error) + } + }) + + t.Run("invalid stale CRL cache, re-download and validate seccessfully", func(t *testing.T) { + // save to cache + if err := memoryCache.Set(context.Background(), "http://example.com", bundle); err != nil { + t.Fatal(err) + } + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ + HTTPClient: &http.Client{ + Transport: expectedRoundTripperMock{Body: crlBytes}, + }, + CacheClient: memoryCache, + }) + if r.Result != result.ResultOK { + t.Fatalf("expected OK, got %s", r.Result) + } + }) + } func TestValidate(t *testing.T) { diff --git a/revocation/ocsp/error.go b/revocation/ocsp/errors.go similarity index 100% rename from revocation/ocsp/error.go rename to revocation/ocsp/errors.go From 75f5f6ca8a17f6daa06709561eb889c88333fa89 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 06:31:53 +0000 Subject: [PATCH 078/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 5 ++++- revocation/crl/cache/file.go | 16 +++++++++------- revocation/crl/cache/file_test.go | 6 ++++-- revocation/crl/cache/memory_test.go | 5 +++-- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index ba728780..d5e0603c 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -59,7 +59,10 @@ type Metadata struct { // // TODO: consider adding DeltaCRL field in the future type Bundle struct { - BaseCRL *x509.RevocationList + // BaseCRL is the parsed base CRL + BaseCRL *x509.RevocationList + + // Metadata is the metadata of the CRL bundle Metadata Metadata } diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 16087235..3c417211 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -143,12 +143,13 @@ func hashURL(url string) string { // // example of metadata.json: // -// { -// "base.crl": { -// "url": "https://example.com/base.crl" -// }, -// "createAt": "2024-07-20T00:00:00Z" -// } +// { +// "base.crl": { +// "url": "https://example.com/base.crl", +// "nextUpdate": "2024-07-20T00:00:00Z" +// }, +// "createAt": "2024-07-20T00:00:00Z" +// } func parseBundleFromTar(data io.Reader) (*Bundle, error) { bundle := &Bundle{} @@ -210,7 +211,8 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { // // { // "base.crl": { -// "url": "https://example.com/base.crl" +// "url": "https://example.com/base.crl", +// "nextUpdate": "2024-07-20T00:00:00Z" // }, // "createAt": "2024-06-30T00:00:00Z" // } diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index 05d8c683..16c341ea 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -23,6 +23,7 @@ import ( "math/big" "os" "path/filepath" + "reflect" "runtime" "strings" "testing" @@ -61,7 +62,7 @@ func TestFileCache(t *testing.T) { key := "testKey" bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreatedAt: time.Now()}} - t.Run("SetAndGet", func(t *testing.T) { + t.Run("SetAndGet comformance", func(t *testing.T) { if err := cache.Set(ctx, key, bundle); err != nil { t.Fatalf("expected no error, got %v", err) } @@ -69,7 +70,8 @@ func TestFileCache(t *testing.T) { if err != nil { t.Fatalf("expected no error, got %v", err) } - if retrievedBundle.Metadata.CreatedAt.Unix() != bundle.Metadata.CreatedAt.Unix() { + + if reflect.DeepEqual(bundle, retrievedBundle) { t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) } }) diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index 06856289..05e84e9d 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -19,6 +19,7 @@ import ( "crypto/x509" "errors" "math/big" + "reflect" "testing" "time" @@ -58,7 +59,7 @@ func TestMemoryCache(t *testing.T) { }, }} key := "testKey" - t.Run("SetAndGet", func(t *testing.T) { + t.Run("SetAndGet comformance test", func(t *testing.T) { if err := cache.Set(ctx, key, bundle); err != nil { t.Fatalf("expected no error, got %v", err) } @@ -66,7 +67,7 @@ func TestMemoryCache(t *testing.T) { if err != nil { t.Fatalf("expected no error, got %v", err) } - if retrievedBundle != bundle { + if reflect.DeepEqual(bundle, retrievedBundle) { t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) } }) From 617bd7fa5b319f7518c0f65f37910d2feb99267f Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 06:41:06 +0000 Subject: [PATCH 079/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 8 -------- revocation/crl/cache/errors.go | 6 ++---- revocation/crl/cache/file.go | 14 ++++++++++---- revocation/revocation.go | 9 +++++---- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index d5e0603c..e6529c73 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -19,14 +19,6 @@ import ( "time" ) -const ( - // PathBaseCRL is the file name of the base CRL - PathBaseCRL = "base.crl" - - // PathMetadata is the file name of the metadata - PathMetadata = "metadata.json" -) - // CRLMetadata stores the URL of the CRL type CRLMetadata struct { // URL stores the URL of the CRL diff --git a/revocation/crl/cache/errors.go b/revocation/crl/cache/errors.go index adfb8a13..f5968297 100644 --- a/revocation/crl/cache/errors.go +++ b/revocation/crl/cache/errors.go @@ -28,7 +28,5 @@ func (e *BrokenFileError) Error() string { return e.Err.Error() } -var ( - // ErrCacheMiss is an error type for when a cache miss occurs - ErrCacheMiss = errors.New("cache miss") -) +// ErrCacheMiss is an error type for when a cache miss occurs +var ErrCacheMiss = errors.New("cache miss") diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 3c417211..87d9bc19 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -28,6 +28,12 @@ import ( ) const ( + // pathBaseCRL is the file name of the base CRL + pathBaseCRL = "base.crl" + + // pathMetadata is the file name of the metadata + pathMetadata = "metadata.json" + // tempFileName is the prefix of the temporary file tempFileName = "notation-*" ) @@ -167,7 +173,7 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { } switch header.Name { - case PathBaseCRL: + case pathBaseCRL: // parse base.crl data, err := io.ReadAll(tar) if err != nil { @@ -182,7 +188,7 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { } } bundle.BaseCRL = baseCRL - case PathMetadata: + case pathMetadata: // parse metadata var metadata Metadata if err := json.NewDecoder(tar).Decode(&metadata); err != nil { @@ -225,7 +231,7 @@ func saveTar(w io.Writer, bundle *Bundle) (err error) { }() // Add base.crl - if err := addToTar(PathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CreatedAt, tarWriter); err != nil { + if err := addToTar(pathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CreatedAt, tarWriter); err != nil { return err } @@ -234,7 +240,7 @@ func saveTar(w io.Writer, bundle *Bundle) (err error) { if err != nil { return err } - return addToTar(PathMetadata, metadataBytes, time.Now(), tarWriter) + return addToTar(pathMetadata, metadataBytes, time.Now(), tarWriter) } func addToTar(fileName string, data []byte, modTime time.Time, tw *tar.Writer) error { diff --git a/revocation/revocation.go b/revocation/revocation.go index b04783ad..a06932aa 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -134,19 +134,20 @@ func NewWithOptions(opts Options) (Validator, error) { return nil, fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose) } - if opts.CRLCache == nil { - memoryCache, err := cache.NewMemoryCache() + crlCache := opts.CRLCache + if crlCache == nil { + newCache, err := cache.NewMemoryCache() if err != nil { return nil, fmt.Errorf("failed to create memory cache: %v", err) } - opts.CRLCache = memoryCache + crlCache = newCache } return &revocation{ ocspHTTPClient: opts.OCSPHTTPClient, crlHTTPClient: opts.CRLHTTPClient, certChainPurpose: opts.CertChainPurpose, - crlCache: opts.CRLCache, + crlCache: crlCache, }, nil } From 0653b0b42ecd3e44927aafada1edeae74aff35bd Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 07:26:07 +0000 Subject: [PATCH 080/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/memory_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index 05e84e9d..b59d79ec 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -67,7 +67,7 @@ func TestMemoryCache(t *testing.T) { if err != nil { t.Fatalf("expected no error, got %v", err) } - if reflect.DeepEqual(bundle, retrievedBundle) { + if !reflect.DeepEqual(bundle, retrievedBundle) { t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) } }) From cf4bb29746afd94133a34a506f20a468bffe96fb Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 07:35:32 +0000 Subject: [PATCH 081/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/cache.go | 3 +++ revocation/crl/fetcher/fetcher.go | 10 +++++----- revocation/crl/fetcher/fetcher_test.go | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index 3497f4b6..c7a40a54 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -36,6 +36,9 @@ import ( const ( // DefaultMaxAge is the default maximum age of the CRLs cache. // If the CRL is older than DefaultMaxAge, it will be considered as expired. + // + // reference: Baseline Requirements for Code-Signing Certificates + // 4.9.7 CRL issuance frequency: https://cabforum.org/uploads/Baseline-Requirements-for-the-Issuance-and-Management-of-Code-Signing.v3.9.pdf DefaultMaxAge = 24 * 7 * time.Hour ) diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher/fetcher.go index 4c9cd55c..76dcd17d 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher/fetcher.go @@ -28,10 +28,10 @@ import ( "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) -// maxCRLSize is the maximum size of CRL in bytes +// MaxCRLSize is the maximum size of CRL in bytes // // CRL examples: https://chasersystems.com/blog/an-analysis-of-certificate-revocation-list-sizes/ -const maxCRLSize = 32 * 1024 * 1024 // 32 MiB +const MaxCRLSize = 32 * 1024 * 1024 // 32 MiB // Fetcher is an interface that specifies methods used for fetching CRL // from the given URL @@ -135,12 +135,12 @@ func download(ctx context.Context, crlURL string, client *http.Client) (bundle * return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) } // read with size limit - data, err := io.ReadAll(io.LimitReader(resp.Body, maxCRLSize)) + data, err := io.ReadAll(io.LimitReader(resp.Body, MaxCRLSize)) if err != nil { return nil, fmt.Errorf("failed to read CRL response: %w", err) } - if len(data) == maxCRLSize { - return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize) + if len(data) == MaxCRLSize { + return nil, fmt.Errorf("CRL size exceeds the limit: %d", MaxCRLSize) } // parse CRL and create bundle diff --git a/revocation/crl/fetcher/fetcher_test.go b/revocation/crl/fetcher/fetcher_test.go index 6f9dfcbc..ac423635 100644 --- a/revocation/crl/fetcher/fetcher_test.go +++ b/revocation/crl/fetcher/fetcher_test.go @@ -215,7 +215,7 @@ func TestDownload(t *testing.T) { t.Run("exceed the size limit", func(t *testing.T) { _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, + Transport: expectedRoundTripperMock{Body: make([]byte, MaxCRLSize+1)}, }) if err == nil { t.Fatal("expected error") From 50fd7e138f674bb3ae8b86bd3b09cd08e87b1dd9 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 13:45:53 +0000 Subject: [PATCH 082/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 10 +- revocation/crl/cache/cache.go | 2 +- revocation/crl/cache/errors.go | 13 -- revocation/crl/cache/errors_test.go | 28 ---- revocation/crl/cache/file.go | 44 +++--- revocation/crl/cache/file_test.go | 14 +- revocation/crl/cache/memory.go | 9 +- revocation/crl/cache/memory_test.go | 25 ++-- revocation/crl/{fetcher => }/fetcher.go | 117 ++++++++-------- revocation/crl/{fetcher => }/fetcher_test.go | 85 +++--------- revocation/internal/crl/crl.go | 56 +++----- revocation/internal/crl/crl_test.go | 134 ++++++++----------- revocation/internal/file/utils.go | 14 ++ revocation/internal/file/utils_test.go | 34 +++++ revocation/revocation.go | 30 ++--- 15 files changed, 257 insertions(+), 358 deletions(-) delete mode 100644 revocation/crl/cache/errors_test.go rename revocation/crl/{fetcher => }/fetcher.go (57%) rename revocation/crl/{fetcher => }/fetcher_test.go (69%) create mode 100644 revocation/internal/file/utils.go create mode 100644 revocation/internal/file/utils_test.go diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index e6529c73..b7ff923f 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -36,13 +36,13 @@ type CRLMetadata struct { // TODO: consider adding DeltaCRL field in the future type Metadata struct { // BaseCRL stores the URL of the base CRL - BaseCRL CRLMetadata `json:"base.crl"` + BaseCRL CRLMetadata `json:"baseCRL"` - // CreatedAt stores the creation time of the CRL bundle. This is different + // CachedAt stores the creation time of the CRL bundle. This is different // from the `ThisUpdate` field in the CRL. The `ThisUpdate` field in the CRL - // is the time when the CRL was generated, while the `CreatedAt` field is for + // is the time when the CRL was generated, while the `CachedAt` field is for // caching purpose, indicating the start of cache effective period. - CreatedAt time.Time `json:"createAt"` + CachedAt time.Time `json:"cachedAt"` } // Bundle is in memory representation of the Bundle tarball, including base CRL @@ -66,7 +66,7 @@ func (b *Bundle) Validate() error { if b.Metadata.BaseCRL.URL == "" { return errors.New("base CRL URL is missing") } - if b.Metadata.CreatedAt.IsZero() { + if b.Metadata.CachedAt.IsZero() { return errors.New("base CRL creation time is missing") } return nil diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go index c7a40a54..4d95e92f 100644 --- a/revocation/crl/cache/cache.go +++ b/revocation/crl/cache/cache.go @@ -39,7 +39,7 @@ const ( // // reference: Baseline Requirements for Code-Signing Certificates // 4.9.7 CRL issuance frequency: https://cabforum.org/uploads/Baseline-Requirements-for-the-Issuance-and-Management-of-Code-Signing.v3.9.pdf - DefaultMaxAge = 24 * 7 * time.Hour + DefaultMaxAge = 7 * 24 * time.Hour ) // Cache is an interface that specifies methods used for caching diff --git a/revocation/crl/cache/errors.go b/revocation/crl/cache/errors.go index f5968297..ad0e8d7d 100644 --- a/revocation/crl/cache/errors.go +++ b/revocation/crl/cache/errors.go @@ -15,18 +15,5 @@ package cache import "errors" -// BrokenFileError is an error type for when parsing a CRL from -// a tarball -// -// This error indicates that the tarball was broken or required data was -// missing -type BrokenFileError struct { - Err error -} - -func (e *BrokenFileError) Error() string { - return e.Err.Error() -} - // ErrCacheMiss is an error type for when a cache miss occurs var ErrCacheMiss = errors.New("cache miss") diff --git a/revocation/crl/cache/errors_test.go b/revocation/crl/cache/errors_test.go deleted file mode 100644 index 4d42cb0c..00000000 --- a/revocation/crl/cache/errors_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// 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 cache - -import ( - "errors" - "testing" -) - -func TestBrokenFileError(t *testing.T) { - innerErr := errors.New("inner error") - err := &BrokenFileError{Err: innerErr} - - if err.Error() != innerErr.Error() { - t.Errorf("expected %q, got %q", innerErr.Error(), err.Error()) - } -} diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index 87d9bc19..fbb20211 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -25,6 +25,8 @@ import ( "os" "path/filepath" "time" + + "github.com/notaryproject/notation-core-go/revocation/internal/file" ) const ( @@ -90,18 +92,14 @@ func (c *FileCache) Get(ctx context.Context, uri string) (bundle *Bundle, err er } return nil, err } - defer func() { - if cerr := f.Close(); cerr != nil && err == nil { - err = cerr - } - }() + defer f.Close() bundle, err = parseBundleFromTar(f) if err != nil { return nil, err } - expires := bundle.Metadata.CreatedAt.Add(c.MaxAge) + expires := bundle.Metadata.CachedAt.Add(c.MaxAge) if c.MaxAge > 0 && time.Now().After(expires) { // do not delete the file to maintain the idempotent behavior return nil, ErrCacheMiss @@ -121,10 +119,9 @@ func (c *FileCache) Set(ctx context.Context, uri string, bundle *Bundle) error { if err != nil { return err } - if err := saveTar(tempFile, bundle); err != nil { - return err - } - tempFile.Close() + file.Using(tempFile, func(f *os.File) error { + return saveTar(tempFile, bundle) + }) // rename is atomic on UNIX-like platforms return os.Rename(tempFile.Name(), filepath.Join(c.root, fileName(uri))) @@ -150,14 +147,14 @@ func hashURL(url string) string { // example of metadata.json: // // { -// "base.crl": { +// "baseCRL": { // "url": "https://example.com/base.crl", // "nextUpdate": "2024-07-20T00:00:00Z" // }, // "createAt": "2024-07-20T00:00:00Z" // } func parseBundleFromTar(data io.Reader) (*Bundle, error) { - bundle := &Bundle{} + var bundle Bundle // parse the tarball tar := tar.NewReader(data) @@ -167,9 +164,7 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { break } if err != nil { - return nil, &BrokenFileError{ - Err: fmt.Errorf("failed to read tarball: %w", err), - } + return nil, fmt.Errorf("failed to read tarball: %w", err) } switch header.Name { @@ -180,21 +175,16 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { return nil, err } - var baseCRL *x509.RevocationList - baseCRL, err = x509.ParseRevocationList(data) + baseCRL, err := x509.ParseRevocationList(data) if err != nil { - return nil, &BrokenFileError{ - Err: fmt.Errorf("failed to parse base CRL from tarball: %w", err), - } + return nil, fmt.Errorf("failed to parse base CRL from tarball: %w", err) } bundle.BaseCRL = baseCRL case pathMetadata: // parse metadata var metadata Metadata if err := json.NewDecoder(tar).Decode(&metadata); err != nil { - return nil, &BrokenFileError{ - Err: fmt.Errorf("failed to parse CRL metadata from tarball: %w", err), - } + return nil, fmt.Errorf("failed to parse CRL metadata from tarball: %w", err) } bundle.Metadata = metadata } @@ -203,7 +193,7 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { return nil, err } - return bundle, nil + return &bundle, nil } // SaveAsTar saves the CRL blob as a tarball, including the base CRL and @@ -216,7 +206,7 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { // example of metadata.json: // // { -// "base.crl": { +// "baseCRL": { // "url": "https://example.com/base.crl", // "nextUpdate": "2024-07-20T00:00:00Z" // }, @@ -231,7 +221,7 @@ func saveTar(w io.Writer, bundle *Bundle) (err error) { }() // Add base.crl - if err := addToTar(pathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CreatedAt, tarWriter); err != nil { + if err := addToTar(pathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CachedAt, tarWriter); err != nil { return err } @@ -240,7 +230,7 @@ func saveTar(w io.Writer, bundle *Bundle) (err error) { if err != nil { return err } - return addToTar(pathMetadata, metadataBytes, time.Now(), tarWriter) + return addToTar(pathMetadata, metadataBytes, bundle.Metadata.CachedAt, tarWriter) } func addToTar(fileName string, data []byte, modTime time.Time, tw *tar.Writer) error { diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go index 16c341ea..200f1261 100644 --- a/revocation/crl/cache/file_test.go +++ b/revocation/crl/cache/file_test.go @@ -61,7 +61,7 @@ func TestFileCache(t *testing.T) { }) key := "testKey" - bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreatedAt: time.Now()}} + bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CachedAt: time.Now()}} t.Run("SetAndGet comformance", func(t *testing.T) { if err := cache.Set(ctx, key, bundle); err != nil { t.Fatalf("expected no error, got %v", err) @@ -77,7 +77,7 @@ func TestFileCache(t *testing.T) { }) t.Run("GetWithExpiredBundle", func(t *testing.T) { - expiredBundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CreatedAt: time.Now().Add(-DefaultMaxAge - 1*time.Second)}} + expiredBundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CachedAt: time.Now().Add(-DefaultMaxAge - 1*time.Second)}} if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { t.Fatalf("expected no error, got %v", err) } @@ -161,7 +161,7 @@ func TestGetFailed(t *testing.T) { t.Run("invalid bundle file", func(t *testing.T) { bundle := &Bundle{ BaseCRL: &x509.RevocationList{Raw: []byte("invalid crl")}, - Metadata: Metadata{CreatedAt: time.Now()}, + Metadata: Metadata{CachedAt: time.Now()}, } if err := saveTar(&bytes.Buffer{}, bundle); err != nil { t.Fatalf("expected no error, got %v", err) @@ -184,7 +184,7 @@ func TestSetFailed(t *testing.T) { } t.Run("failed to save tarball", func(t *testing.T) { - bundle := &Bundle{Metadata: Metadata{CreatedAt: time.Now()}} + bundle := &Bundle{Metadata: Metadata{CachedAt: time.Now()}} if err := cache.Set(context.Background(), "invalid.tar", bundle); err == nil { t.Fatalf("expected error, got nil") } @@ -214,7 +214,7 @@ func TestParseAndSave(t *testing.T) { BaseCRL: CRLMetadata{ URL: exampleURL, }, - CreatedAt: time.Now(), + CachedAt: time.Now(), }, } @@ -238,7 +238,7 @@ func TestParseAndSave(t *testing.T) { t.Errorf("expected URL to be %s, got %s", exampleURL, bundle.Metadata.BaseCRL.URL) } - if bundle.Metadata.CreatedAt.IsZero() { + if bundle.Metadata.CachedAt.IsZero() { t.Errorf("expected CreateAt to be set, got zero value") } }) @@ -335,7 +335,7 @@ func TestSaveTarFailed(t *testing.T) { BaseCRL: CRLMetadata{ URL: "https://example.com/base.crl", }, - CreatedAt: time.Now(), + CachedAt: time.Now(), }, } if err := saveTar(&errorWriter{}, bundle); err == nil { diff --git a/revocation/crl/cache/memory.go b/revocation/crl/cache/memory.go index 6f42ba8a..def56b99 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/crl/cache/memory.go @@ -37,13 +37,10 @@ type MemoryCache struct { } // NewMemoryCache creates a new memory store. -// -// - maxAge is the maximum age of the CRLs cache. If the CRL is older than -// maxAge, it will be considered as expired. -func NewMemoryCache() (*MemoryCache, error) { +func NewMemoryCache() *MemoryCache { return &MemoryCache{ MaxAge: DefaultMaxAge, - }, nil + } } // Get retrieves the CRL from the memory store. @@ -60,7 +57,7 @@ func (c *MemoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { return nil, fmt.Errorf("invalid type: %T", value) } - expires := bundle.Metadata.CreatedAt.Add(c.MaxAge) + expires := bundle.Metadata.CachedAt.Add(c.MaxAge) if c.MaxAge > 0 && time.Now().After(expires) { return nil, ErrCacheMiss } diff --git a/revocation/crl/cache/memory_test.go b/revocation/crl/cache/memory_test.go index b59d79ec..311d36be 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/crl/cache/memory_test.go @@ -42,10 +42,7 @@ func TestMemoryCache(t *testing.T) { } // Test NewMemoryCache - cache, err := NewMemoryCache() - if err != nil { - t.Fatalf("expected no error, got %v", err) - } + cache := NewMemoryCache() if cache.MaxAge != DefaultMaxAge { t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.MaxAge) } @@ -53,7 +50,7 @@ func TestMemoryCache(t *testing.T) { bundle := &Bundle{ BaseCRL: baseCRL, Metadata: Metadata{ - CreatedAt: time.Now(), + CachedAt: time.Now(), BaseCRL: CRLMetadata{ URL: "http://crl", }, @@ -76,7 +73,7 @@ func TestMemoryCache(t *testing.T) { expiredBundle := &Bundle{ BaseCRL: baseCRL, Metadata: Metadata{ - CreatedAt: time.Now().Add(-DefaultMaxAge - 1*time.Second), + CachedAt: time.Now().Add(-DefaultMaxAge - 1*time.Second), BaseCRL: CRLMetadata{ URL: "http://crl", }, @@ -107,31 +104,25 @@ func TestMemoryCacheFailed(t *testing.T) { // Test Get with invalid type t.Run("GetWithInvalidType", func(t *testing.T) { - cache, err := NewMemoryCache() - if err != nil { - t.Fatalf("expected no error, got %v", err) - } + cache := NewMemoryCache() cache.store.Store("invalidKey", "invalidValue") - _, err = cache.Get(ctx, "invalidKey") + _, err := cache.Get(ctx, "invalidKey") if err == nil { t.Fatalf("expected error, got nil") } }) t.Run("ValidateFailed", func(t *testing.T) { - cache, err := NewMemoryCache() - if err != nil { - t.Fatalf("expected no error, got %v", err) - } + cache := NewMemoryCache() bundle := &Bundle{ BaseCRL: nil, Metadata: Metadata{ - CreatedAt: time.Now(), + CachedAt: time.Now(), BaseCRL: CRLMetadata{ URL: "http://crl", }, }} - err = cache.Set(ctx, "invalidBundle", bundle) + err := cache.Set(ctx, "invalidBundle", bundle) if err == nil { t.Fatalf("expected error, got nil") } diff --git a/revocation/crl/fetcher/fetcher.go b/revocation/crl/fetcher.go similarity index 57% rename from revocation/crl/fetcher/fetcher.go rename to revocation/crl/fetcher.go index 76dcd17d..17a2339d 100644 --- a/revocation/crl/fetcher/fetcher.go +++ b/revocation/crl/fetcher.go @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package fetcher provides Fetcher interface and its implementation to fetch +// Package crl provides Fetcher interface and its implementation to fetch // CRL from the given URL -package fetcher +package crl import ( "context" @@ -37,33 +37,25 @@ const MaxCRLSize = 32 * 1024 * 1024 // 32 MiB // from the given URL type Fetcher interface { // Fetch retrieves the CRL from the given URL. - Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) - - // Download downloads the CRL from the given URL and saves it to the - // cache. - Download(ctx context.Context, crlURL string) (bundle *cache.Bundle, err error) + Fetch(ctx context.Context, uri string) (base, delta *x509.RevocationList, err error) } -type cachedFetcher struct { - httpClient *http.Client - cacheClient cache.Cache +type HTTPFetcher struct { + // Cache stores fetched CRLs and reuses them with the max ages. + // If Cache is nil, no cache is used. + Cache cache.Cache + + httpClient *http.Client } -// NewCachedFetcher creates a new Fetcher with the given HTTP client and cache client -// - if httpClient is nil, http.DefaultClient will be used -func NewCachedFetcher(httpClient *http.Client, cacheClient cache.Cache) (Fetcher, error) { +func NewHTTPFetcher(httpClient *http.Client) *HTTPFetcher { if httpClient == nil { httpClient = http.DefaultClient } - if cacheClient == nil { - return nil, errors.New("cache client is nil") + return &HTTPFetcher{ + httpClient: httpClient, } - - return &cachedFetcher{ - httpClient: httpClient, - cacheClient: cacheClient, - }, nil } // Fetch retrieves the CRL from the given URL @@ -71,47 +63,72 @@ func NewCachedFetcher(httpClient *http.Client, cacheClient cache.Cache) (Fetcher // Steps: // 1. Try to get from cache // 2. If not exist or broken, download and save to cache -func (f *cachedFetcher) Fetch(ctx context.Context, crlURL string) (bundle *cache.Bundle, fromCache bool, err error) { - if crlURL == "" { - return nil, false, errors.New("CRL URL is empty") +func (f *HTTPFetcher) Fetch(ctx context.Context, uri string) (base, delta *x509.RevocationList, err error) { + if uri == "" { + return nil, nil, errors.New("CRL URL is empty") + } + + if f.Cache == nil { + // no cache, download directly + base, err := f.download(ctx, uri) + return base, nil, err } // try to get from cache - bundle, err = f.cacheClient.Get(ctx, crlURL) + bundle, err := f.Cache.Get(ctx, uri) if err != nil { - var cacheBrokenError *cache.BrokenFileError - if errors.Is(err, cache.ErrCacheMiss) || - errors.As(err, &cacheBrokenError) { - bundle, err = f.Download(ctx, crlURL) - if err != nil { - return nil, false, err - } - return bundle, false, nil + base, err := f.download(ctx, uri) + if err != nil { + return nil, nil, err } + return base, nil, nil + } - return nil, false, err + // validate NextUpdate + nextUpdate := bundle.Metadata.BaseCRL.NextUpdate + if !nextUpdate.IsZero() && time.Now().After(nextUpdate) { + // download and save to cache + base, err := f.download(ctx, uri) + if err != nil { + return nil, nil, err + } + return base, nil, nil } - return bundle, true, nil + return bundle.BaseCRL, nil, nil } // Download downloads the CRL from the given URL and saves it to the // cache -func (f *cachedFetcher) Download(ctx context.Context, crlURL string) (bundle *cache.Bundle, err error) { - bundle, err = download(ctx, crlURL, f.httpClient) +func (f *HTTPFetcher) download(ctx context.Context, uri string) (base *x509.RevocationList, err error) { + base, err = download(ctx, uri, f.httpClient) if err != nil { return nil, err } - // save to cache - if err := f.cacheClient.Set(ctx, crlURL, bundle); err != nil { - return nil, fmt.Errorf("failed to save to cache: %w", err) + if f.Cache == nil { + // no cache, return directly + return base, nil } - return bundle, nil + bundle := &cache.Bundle{ + BaseCRL: base, + Metadata: cache.Metadata{ + BaseCRL: cache.CRLMetadata{ + URL: uri, + NextUpdate: base.NextUpdate, + }, + CachedAt: time.Now(), + }, + } + + // ignore the error, as the cache is not critical + _ = f.Cache.Set(ctx, uri, bundle) + + return base, nil } -func download(ctx context.Context, crlURL string, client *http.Client) (bundle *cache.Bundle, err error) { +func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { // validate URL parsedURL, err := url.Parse(crlURL) if err != nil { @@ -143,20 +160,6 @@ func download(ctx context.Context, crlURL string, client *http.Client) (bundle * return nil, fmt.Errorf("CRL size exceeds the limit: %d", MaxCRLSize) } - // parse CRL and create bundle - crl, err := x509.ParseRevocationList(data) - if err != nil { - return nil, fmt.Errorf("failed to parse CRL: %w", err) - } - - return &cache.Bundle{ - BaseCRL: crl, - Metadata: cache.Metadata{ - BaseCRL: cache.CRLMetadata{ - URL: crlURL, - NextUpdate: crl.NextUpdate, - }, - CreatedAt: time.Now(), - }, - }, nil + // parse CRL + return x509.ParseRevocationList(data) } diff --git a/revocation/crl/fetcher/fetcher_test.go b/revocation/crl/fetcher_test.go similarity index 69% rename from revocation/crl/fetcher/fetcher_test.go rename to revocation/crl/fetcher_test.go index ac423635..17c0a4a1 100644 --- a/revocation/crl/fetcher/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package fetcher +package crl import ( "bytes" @@ -29,32 +29,15 @@ import ( "github.com/notaryproject/notation-core-go/testhelper" ) -func TestNewCachedFetcher(t *testing.T) { - c, err := cache.NewMemoryCache() - if err != nil { - t.Errorf("NewMemoryCache() error = %v, want nil", err) - } +func TestNewHTTPFetcher(t *testing.T) { t.Run("httpClient is nil", func(t *testing.T) { - _, err := NewCachedFetcher(nil, c) - if err != nil { - t.Errorf("NewCachedFetcher() error = %v, want nil", err) - } - }) - - t.Run("cacheClient is nil", func(t *testing.T) { - _, err := NewCachedFetcher(nil, nil) - if err == nil { - t.Errorf("NewCachedFetcher() error = nil, want not nil") - } + _ = NewHTTPFetcher(nil) }) } func TestFetch(t *testing.T) { // prepare cache - c, err := cache.NewMemoryCache() - if err != nil { - t.Errorf("NewMemoryCache() error = %v, want nil", err) - } + c := cache.NewMemoryCache() // prepare crl certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) @@ -77,7 +60,7 @@ func TestFetch(t *testing.T) { BaseCRL: cache.CRLMetadata{ URL: exampleURL, }, - CreatedAt: time.Now(), + CachedAt: time.Now(), }, } if err := c.Set(context.Background(), exampleURL, bundle); err != nil { @@ -85,10 +68,8 @@ func TestFetch(t *testing.T) { } t.Run("url is empty", func(t *testing.T) { - f, err := NewCachedFetcher(nil, c) - if err != nil { - t.Errorf("NewCachedFetcher() error = %v, want nil", err) - } + f := NewHTTPFetcher(nil) + f.Cache = c _, _, err = f.Fetch(context.Background(), "") if err == nil { t.Errorf("Fetcher.Fetch() error = nil, want not nil") @@ -96,25 +77,14 @@ func TestFetch(t *testing.T) { }) t.Run("cache hit", func(t *testing.T) { - f, err := NewCachedFetcher(nil, c) - if err != nil { - t.Errorf("NewCachedFetcher() error = %v, want nil", err) - } - fetchedBundle, fromCache, err := f.Fetch(context.Background(), exampleURL) + f := NewHTTPFetcher(nil) + f.Cache = c + base, _, err := f.Fetch(context.Background(), exampleURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } - if !fromCache { - t.Errorf("Fetcher.Fetch() fromCache = false, want true") - } - if fetchedBundle == nil { - t.Errorf("Fetcher.Fetch() fetchedBundle = nil, want not nil") - } - if fetchedBundle != nil && fetchedBundle.Metadata.BaseCRL.URL != exampleURL { - t.Errorf("Fetcher.Fetch() fetchedBundle.Metadata.BaseCRL.URL = %v, want %v", fetchedBundle.Metadata.BaseCRL.URL, exampleURL) - } - if !bytes.Equal(fetchedBundle.BaseCRL.Raw, baseCRL.Raw) { - t.Errorf("Fetcher.Fetch() fetchedBundle.BaseCRL.Raw = %v, want %v", fetchedBundle.BaseCRL.Raw, baseCRL.Raw) + if !bytes.Equal(base.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) } }) @@ -122,25 +92,17 @@ func TestFetch(t *testing.T) { httpClient := &http.Client{ Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, } - f, err := NewCachedFetcher(httpClient, c) - if err != nil { - t.Errorf("NewCachedFetcher() error = %v, want nil", err) - } - fetchedBundle, fromCache, err := f.Fetch(context.Background(), uncachedURL) + f := NewHTTPFetcher(httpClient) + f.Cache = c + base, _, err := f.Fetch(context.Background(), uncachedURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } - if fromCache { - t.Errorf("Fetcher.Fetch() fromCache = true, want false") - } - if fetchedBundle == nil { + if base == nil { t.Errorf("Fetcher.Fetch() fetchedBundle = nil, want not nil") } - if fetchedBundle != nil && fetchedBundle.Metadata.BaseCRL.URL != uncachedURL { - t.Errorf("Fetcher.Fetch() fetchedBundle.Metadata.BaseCRL.URL = %v, want %v", fetchedBundle.Metadata.BaseCRL.URL, exampleURL) - } - if !bytes.Equal(fetchedBundle.BaseCRL.Raw, baseCRL.Raw) { - t.Errorf("Fetcher.Fetch() fetchedBundle.BaseCRL.Raw = %v, want %v", fetchedBundle.BaseCRL.Raw, baseCRL.Raw) + if !bytes.Equal(base.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) } }) @@ -148,14 +110,9 @@ func TestFetch(t *testing.T) { httpClient := &http.Client{ Transport: errorRoundTripperMock{}, } - newCache, err := cache.NewMemoryCache() - if err != nil { - t.Errorf("NewMemoryCache() error = %v, want nil", err) - } - f, err := NewCachedFetcher(httpClient, newCache) - if err != nil { - t.Errorf("NewCachedFetcher() error = %v, want nil", err) - } + newCache := cache.NewMemoryCache() + f := NewHTTPFetcher(httpClient) + f.Cache = newCache _, _, err = f.Fetch(context.Background(), uncachedURL) if err == nil { t.Errorf("Fetcher.Fetch() error = nil, want not nil") diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index 3b78f3ab..72d89a4e 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -21,11 +21,9 @@ import ( "encoding/asn1" "errors" "fmt" - "net/http" "time" - "github.com/notaryproject/notation-core-go/revocation/crl/cache" - "github.com/notaryproject/notation-core-go/revocation/crl/fetcher" + "github.com/notaryproject/notation-core-go/revocation/crl" "github.com/notaryproject/notation-core-go/revocation/result" ) @@ -45,15 +43,12 @@ var ( // CertCheckStatusOptions specifies values that are needed to check CRL type CertCheckStatusOptions struct { - // HTTPClient is the HTTP client used to download CRL - HTTPClient *http.Client + // HTTPClient is the HTTP client used to download the CRL + Fetcher crl.Fetcher // SigningTime is used to compare with the invalidity date during revocation // check SigningTime time.Time - - // cacheClient is the cache client used to store the CRL - CacheClient cache.Cache } // CertCheckStatus checks the revocation status of a certificate using CRL @@ -78,25 +73,24 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C } } - var ( - serverResults = make([]*result.ServerResult, 0, len(cert.CRLDistributionPoints)) - lastErr error - crlURL string - ) - - cachedFetcher, err := fetcher.NewCachedFetcher(opts.HTTPClient, opts.CacheClient) - if err != nil { + if opts.Fetcher == nil { return &result.CertRevocationResult{ Result: result.ResultUnknown, ServerResults: []*result.ServerResult{{ - Result: result.ResultUnknown, - Error: err, RevocationMethod: result.RevocationMethodCRL, + Error: errors.New("CRL fetcher is nil"), + Result: result.ResultUnknown, }}, RevocationMethod: result.RevocationMethodCRL, } } + var ( + serverResults = make([]*result.ServerResult, 0, len(cert.CRLDistributionPoints)) + lastErr error + crlURL string + ) + // The CRLDistributionPoints contains the URIs of all the CRL distribution // points. Since it does not distinguish the reason field, it needs to check // all the URIs to avoid missing any partial CRLs. @@ -105,33 +99,19 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C // point with one CRL URI, which will be cached, so checking all the URIs is // not a performance issue. for _, crlURL = range cert.CRLDistributionPoints { - bundle, fromCache, err := cachedFetcher.Fetch(ctx, crlURL) + // ignore delta CRL as it is not implemented + base, _, err := opts.Fetcher.Fetch(ctx, crlURL) if err != nil { lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err) break } - if err = validate(bundle.BaseCRL, issuer); err != nil { - if fromCache { - // the CRL may be stale, try to download again - bundle, err = cachedFetcher.Download(ctx, crlURL) - if err != nil { - lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err) - break - } - - if err = validate(bundle.BaseCRL, issuer); err != nil { - lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err) - break - } - } else { - // the CRL is fresh, but it is invalid - lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err) - break - } + if err = validate(base, issuer); err != nil { + lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err) + break } - crlResult, err := checkRevocation(cert, bundle.BaseCRL, opts.SigningTime, crlURL) + crlResult, err := checkRevocation(cert, base, opts.SigningTime, crlURL) if err != nil { lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err) break diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index b1fe4613..ec869bfa 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -29,6 +29,7 @@ import ( "testing" "time" + crlutils "github.com/notaryproject/notation-core-go/revocation/crl" "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/result" "github.com/notaryproject/notation-core-go/testhelper" @@ -44,39 +45,38 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("download error", func(t *testing.T) { - memoryCache, err := cache.NewMemoryCache() - if err != nil { - t.Fatal(err) - } + memoryCache := cache.NewMemoryCache() cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, } + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: errorRoundTripperMock{}}, + ) + fetcher.Cache = memoryCache + r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: errorRoundTripperMock{}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) + if r.ServerResults[0].Error == nil { t.Fatal("expected error") } }) t.Run("CRL validate failed", func(t *testing.T) { - memoryCache, err := cache.NewMemoryCache() - if err != nil { - t.Fatal(err) - } + memoryCache := cache.NewMemoryCache() cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, } + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: expiredCRLRoundTripperMock{}}, + ) + fetcher.Cache = memoryCache + r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: expiredCRLRoundTripperMock{}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) if r.ServerResults[0].Error == nil { t.Fatal("expected error") @@ -89,10 +89,7 @@ func TestCertCheckStatus(t *testing.T) { issuerKey := chain[1].PrivateKey t.Run("revoked", func(t *testing.T) { - memoryCache, err := cache.NewMemoryCache() - if err != nil { - t.Fatal(err) - } + memoryCache := cache.NewMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -108,11 +105,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, + ) + fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: expectedRoundTripperMock{Body: crlBytes}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) if r.Result != result.ResultRevoked { t.Fatalf("expected revoked, got %s", r.Result) @@ -120,10 +118,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("unknown critical extension", func(t *testing.T) { - memoryCache, err := cache.NewMemoryCache() - if err != nil { - t.Fatal(err) - } + memoryCache := cache.NewMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -145,11 +140,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, + ) + fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: expectedRoundTripperMock{Body: crlBytes}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) if r.ServerResults[0].Error == nil { t.Fatal("expected error") @@ -157,10 +153,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("Not revoked", func(t *testing.T) { - memoryCache, err := cache.NewMemoryCache() - if err != nil { - t.Fatal(err) - } + memoryCache := cache.NewMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -170,11 +163,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, + ) + fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: expectedRoundTripperMock{Body: crlBytes}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) if r.Result != result.ResultOK { t.Fatalf("expected OK, got %s", r.Result) @@ -182,10 +176,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL with delta CRL is not checked", func(t *testing.T) { - memoryCache, err := cache.NewMemoryCache() - if err != nil { - t.Fatal(err) - } + memoryCache := cache.NewMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -200,33 +191,19 @@ func TestCertCheckStatus(t *testing.T) { if err != nil { t.Fatal(err) } - + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, + ) + fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: expectedRoundTripperMock{Body: crlBytes}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) if !errors.Is(r.ServerResults[0].Error, ErrDeltaCRLNotSupported) { t.Fatal("expected ErrDeltaCRLNotChecked") } }) - t.Run("CRL cache is nil", func(t *testing.T) { - r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: expectedRoundTripperMock{Body: []byte{}}, - }, - }) - if r.ServerResults[0].Error.Error() != "cache client is nil" { - t.Fatal("expected error") - } - }) - - memoryCache, err := cache.NewMemoryCache() - if err != nil { - t.Fatal(err) - } + memoryCache := cache.NewMemoryCache() // create a stale CRL crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ @@ -247,7 +224,7 @@ func TestCertCheckStatus(t *testing.T) { URL: "http://example.com", NextUpdate: crl.NextUpdate, }, - CreatedAt: time.Now(), + CachedAt: time.Now(), }, } chain[0].Cert.CRLDistributionPoints = []string{"http://example.com"} @@ -258,11 +235,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: errorRoundTripperMock{}}, + ) + fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: errorRoundTripperMock{}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) if !strings.HasPrefix(r.ServerResults[0].Error.Error(), "failed to download CRL from") { t.Fatalf("unexpected error, got %v", r.ServerResults[0].Error) @@ -275,11 +253,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, + ) + fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: expectedRoundTripperMock{Body: crlBytes}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) if !strings.HasPrefix(r.ServerResults[0].Error.Error(), "failed to validate CRL from") { t.Fatalf("unexpected error, got %v", r.ServerResults[0].Error) @@ -300,11 +279,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } + fetcher := crlutils.NewHTTPFetcher( + &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, + ) + fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ - HTTPClient: &http.Client{ - Transport: expectedRoundTripperMock{Body: crlBytes}, - }, - CacheClient: memoryCache, + Fetcher: fetcher, }) if r.Result != result.ResultOK { t.Fatalf("expected OK, got %s", r.Result) diff --git a/revocation/internal/file/utils.go b/revocation/internal/file/utils.go new file mode 100644 index 00000000..522f4f5a --- /dev/null +++ b/revocation/internal/file/utils.go @@ -0,0 +1,14 @@ +package file + +import "io" + +// Using is a helper function to ensure that a resource is closed after using it +// and return the error if any. +func Using[T io.Closer](t T, f func(t T) error) (err error) { + defer func() { + if closeErr := t.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() + return f(t) +} diff --git a/revocation/internal/file/utils_test.go b/revocation/internal/file/utils_test.go new file mode 100644 index 00000000..8e78b1c1 --- /dev/null +++ b/revocation/internal/file/utils_test.go @@ -0,0 +1,34 @@ +package file + +import ( + "errors" + "testing" +) + +func TestUsing(t *testing.T) { + t.Run("Close error", func(t *testing.T) { + err := Using(&mockCloser{err: errors.New("closer error")}, func(t *mockCloser) error { + return nil + }) + if err.Error() != "closer error" { + t.Fatalf("expected closer error, got %v", err) + } + }) + + t.Run("Close without error", func(t *testing.T) { + err := Using(&mockCloser{}, func(t *mockCloser) error { + return nil + }) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + }) +} + +type mockCloser struct { + err error +} + +func (m *mockCloser) Close() error { + return m.err +} diff --git a/revocation/revocation.go b/revocation/revocation.go index a06932aa..88e0aaaa 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -24,6 +24,7 @@ import ( "sync" "time" + crlutils "github.com/notaryproject/notation-core-go/revocation/crl" "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/internal/crl" "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" @@ -83,16 +84,12 @@ func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } - memoryCache, err := cache.NewMemoryCache() - if err != nil { - return nil, fmt.Errorf("failed to create memory cache: %v", err) - } return &revocation{ ocspHTTPClient: httpClient, crlHTTPClient: httpClient, certChainPurpose: purpose.CodeSigning, - crlCache: memoryCache, + crlCache: cache.NewMemoryCache(), }, nil } @@ -114,7 +111,11 @@ type Options struct { CertChainPurpose purpose.Purpose // CRLCache is the cache client used to store the CRL. if not provided, - // a default in-memory cache will be used. + // no cache will be used. + // + // The cache package provides built-in cache implementations: + // - cache.NewMemoryCache: in-memory cache + // - cache.NewFileCache: file-based cache CRLCache cache.Cache } @@ -134,20 +135,11 @@ func NewWithOptions(opts Options) (Validator, error) { return nil, fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose) } - crlCache := opts.CRLCache - if crlCache == nil { - newCache, err := cache.NewMemoryCache() - if err != nil { - return nil, fmt.Errorf("failed to create memory cache: %v", err) - } - crlCache = newCache - } - return &revocation{ ocspHTTPClient: opts.OCSPHTTPClient, crlHTTPClient: opts.CRLHTTPClient, certChainPurpose: opts.CertChainPurpose, - crlCache: crlCache, + crlCache: opts.CRLCache, }, nil } @@ -192,10 +184,12 @@ func (r *revocation) ValidateContext(ctx context.Context, validateContextOpts Va HTTPClient: r.ocspHTTPClient, SigningTime: validateContextOpts.AuthenticSigningTime, } + + fetcher := crlutils.NewHTTPFetcher(r.crlHTTPClient) + fetcher.Cache = r.crlCache crlOpts := crl.CertCheckStatusOptions{ - HTTPClient: r.crlHTTPClient, + Fetcher: fetcher, SigningTime: validateContextOpts.AuthenticSigningTime, - CacheClient: r.crlCache, } // panicChain is used to store the panic in goroutine and handle it From 65345ba5916d8efd929160bb58b433bcf595b3b8 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 14:19:19 +0000 Subject: [PATCH 083/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 27 +++++--------- revocation/crl/fetcher_test.go | 56 +++++++++++++++++++++++++++-- revocation/internal/crl/crl_test.go | 15 ++++++-- 3 files changed, 73 insertions(+), 25 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 17a2339d..088406fe 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -70,29 +70,19 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, uri string) (base, delta *x509. if f.Cache == nil { // no cache, download directly - base, err := f.download(ctx, uri) - return base, nil, err + return f.download(ctx, uri) } // try to get from cache bundle, err := f.Cache.Get(ctx, uri) if err != nil { - base, err := f.download(ctx, uri) - if err != nil { - return nil, nil, err - } - return base, nil, nil + return f.download(ctx, uri) } - // validate NextUpdate + // check expiry nextUpdate := bundle.Metadata.BaseCRL.NextUpdate if !nextUpdate.IsZero() && time.Now().After(nextUpdate) { - // download and save to cache - base, err := f.download(ctx, uri) - if err != nil { - return nil, nil, err - } - return base, nil, nil + return f.download(ctx, uri) } return bundle.BaseCRL, nil, nil @@ -100,15 +90,15 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, uri string) (base, delta *x509. // Download downloads the CRL from the given URL and saves it to the // cache -func (f *HTTPFetcher) download(ctx context.Context, uri string) (base *x509.RevocationList, err error) { +func (f *HTTPFetcher) download(ctx context.Context, uri string) (base, delta *x509.RevocationList, err error) { base, err = download(ctx, uri, f.httpClient) if err != nil { - return nil, err + return nil, nil, err } if f.Cache == nil { // no cache, return directly - return base, nil + return base, delta, nil } bundle := &cache.Bundle{ @@ -121,11 +111,10 @@ func (f *HTTPFetcher) download(ctx context.Context, uri string) (base *x509.Revo CachedAt: time.Now(), }, } - // ignore the error, as the cache is not critical _ = f.Cache.Set(ctx, uri, bundle) - return base, nil + return base, delta, nil } func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 17c0a4a1..8fe05eb0 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -76,6 +76,20 @@ func TestFetch(t *testing.T) { } }) + t.Run("fetch without cache", func(t *testing.T) { + httpClient := &http.Client{ + Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, + } + f := NewHTTPFetcher(httpClient) + base, _, err := f.Fetch(context.Background(), exampleURL) + if err != nil { + t.Errorf("Fetcher.Fetch() error = %v, want nil", err) + } + if !bytes.Equal(base.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) + } + }) + t.Run("cache hit", func(t *testing.T) { f := NewHTTPFetcher(nil) f.Cache = c @@ -98,9 +112,6 @@ func TestFetch(t *testing.T) { if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } - if base == nil { - t.Errorf("Fetcher.Fetch() fetchedBundle = nil, want not nil") - } if !bytes.Equal(base.Raw, baseCRL.Raw) { t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) } @@ -118,6 +129,45 @@ func TestFetch(t *testing.T) { t.Errorf("Fetcher.Fetch() error = nil, want not nil") } }) + + t.Run("cache expired", func(t *testing.T) { + expiredBundle := &cache.Bundle{ + BaseCRL: baseCRL, + Metadata: cache.Metadata{ + BaseCRL: cache.CRLMetadata{ + URL: exampleURL, + NextUpdate: time.Now().Add(-1 * time.Hour), + }, + CachedAt: time.Now().Add(-1 * time.Hour), + }, + } + if err := c.Set(context.Background(), exampleURL, expiredBundle); err != nil { + t.Errorf("Cache.Set() error = %v, want nil", err) + } + + // generate a new CRL + // prepare crl + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + newCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + + httpClient := &http.Client{ + Transport: expectedRoundTripperMock{Body: newCRLBytes}, + } + f := NewHTTPFetcher(httpClient) + f.Cache = c + base, _, err := f.Fetch(context.Background(), exampleURL) + if err != nil { + t.Errorf("Fetcher.Fetch() error = %v, want nil", err) + } + if !bytes.Equal(base.Raw, newCRLBytes) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) + } + }) } func TestDownload(t *testing.T) { diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index ec869bfa..9390ca32 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -39,8 +39,18 @@ func TestCertCheckStatus(t *testing.T) { t.Run("certificate does not have CRLDistributionPoints", func(t *testing.T) { cert := &x509.Certificate{} r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{}) - if r.Result != result.ResultNonRevokable { - t.Fatalf("expected NonRevokable, got %s", r.Result) + if r.ServerResults[0].Error.Error() != "CRL is not supported" { + t.Fatalf("expected CRL is not supported, got %v", r.ServerResults[0].Error) + } + }) + + t.Run("fetcher is nil", func(t *testing.T) { + cert := &x509.Certificate{ + CRLDistributionPoints: []string{"http://example.com"}, + } + r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{}) + if r.ServerResults[0].Error.Error() != "CRL fetcher is nil" { + t.Fatalf("expected CRL fetcher is nil, got %v", r.ServerResults[0].Error) } }) @@ -290,7 +300,6 @@ func TestCertCheckStatus(t *testing.T) { t.Fatalf("expected OK, got %s", r.Result) } }) - } func TestValidate(t *testing.T) { From 5637752a6ef4e2b1eaceee9065de8412b1e9cad9 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 14:21:52 +0000 Subject: [PATCH 084/110] docs: add license Signed-off-by: Junjie Gao --- revocation/internal/file/utils.go | 14 ++++++++++++++ revocation/internal/file/utils_test.go | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/revocation/internal/file/utils.go b/revocation/internal/file/utils.go index 522f4f5a..0a374239 100644 --- a/revocation/internal/file/utils.go +++ b/revocation/internal/file/utils.go @@ -1,3 +1,17 @@ +// 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 file provides utilities for file operations. package file import "io" diff --git a/revocation/internal/file/utils_test.go b/revocation/internal/file/utils_test.go index 8e78b1c1..abf786dd 100644 --- a/revocation/internal/file/utils_test.go +++ b/revocation/internal/file/utils_test.go @@ -1,3 +1,16 @@ +// 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 file import ( From e9f3b9d25c0a0d5f1e9268ccd5c71d831a602990 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 14:23:52 +0000 Subject: [PATCH 085/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache/file.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go index fbb20211..5c47ea57 100644 --- a/revocation/crl/cache/file.go +++ b/revocation/crl/cache/file.go @@ -151,7 +151,7 @@ func hashURL(url string) string { // "url": "https://example.com/base.crl", // "nextUpdate": "2024-07-20T00:00:00Z" // }, -// "createAt": "2024-07-20T00:00:00Z" +// "cachedAt": "2024-07-20T00:00:00Z" // } func parseBundleFromTar(data io.Reader) (*Bundle, error) { var bundle Bundle @@ -210,7 +210,7 @@ func parseBundleFromTar(data io.Reader) (*Bundle, error) { // "url": "https://example.com/base.crl", // "nextUpdate": "2024-07-20T00:00:00Z" // }, -// "createAt": "2024-06-30T00:00:00Z" +// "cachedAt": "2024-06-30T00:00:00Z" // } func saveTar(w io.Writer, bundle *Bundle) (err error) { tarWriter := tar.NewWriter(w) From a3c3c8420dba6fb0b9b55138a8c9d177f8425753 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 18 Sep 2024 23:50:05 +0000 Subject: [PATCH 086/110] fix: remove cache implementation Signed-off-by: Junjie Gao --- revocation/crl/cache/bundle.go | 2 +- revocation/crl/cache/bundle_test.go | 85 +---- revocation/crl/cache/file.go | 248 ------------ revocation/crl/cache/file_test.go | 358 ------------------ revocation/crl/fetcher_test.go | 5 +- .../cache => internal/cachehelper}/memory.go | 16 +- .../cachehelper}/memory_test.go | 49 +-- revocation/internal/crl/crl_test.go | 15 +- revocation/revocation.go | 3 +- 9 files changed, 68 insertions(+), 713 deletions(-) delete mode 100644 revocation/crl/cache/file.go delete mode 100644 revocation/crl/cache/file_test.go rename revocation/{crl/cache => internal/cachehelper}/memory.go (85%) rename revocation/{crl/cache => internal/cachehelper}/memory_test.go (72%) diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go index b7ff923f..fe73274e 100644 --- a/revocation/crl/cache/bundle.go +++ b/revocation/crl/cache/bundle.go @@ -67,7 +67,7 @@ func (b *Bundle) Validate() error { return errors.New("base CRL URL is missing") } if b.Metadata.CachedAt.IsZero() { - return errors.New("base CRL creation time is missing") + return errors.New("base CRL CachedAt is missing") } return nil } diff --git a/revocation/crl/cache/bundle_test.go b/revocation/crl/cache/bundle_test.go index 8f5fd5b7..5220887f 100644 --- a/revocation/crl/cache/bundle_test.go +++ b/revocation/crl/cache/bundle_test.go @@ -14,13 +14,10 @@ package cache import ( - "archive/tar" - "bytes" "crypto/rand" "crypto/x509" "math/big" "testing" - "time" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -33,78 +30,38 @@ func TestValidate(t *testing.T) { if err != nil { t.Fatalf("failed to create base CRL: %v", err) } + base, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } t.Run("missing BaseCRL", func(t *testing.T) { - var buf bytes.Buffer - _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") + var bundle Bundle + if err := bundle.Validate(); err.Error() != "base CRL is missing" { + t.Fatalf("expected base CRL is missing, got %v", err) } }) t.Run("missing metadata baseCRL URL", func(t *testing.T) { - var buf bytes.Buffer - tw := tar.NewWriter(&buf) - baseCRLHeader := &tar.Header{ - Name: "base.crl", - Size: int64(len(crlBytes)), - Mode: 0644, - ModTime: time.Now(), - } - if err := tw.WriteHeader(baseCRLHeader); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Write(crlBytes) - - metadataContent := []byte(`{"base.crl": {}}`) - metadataHeader := &tar.Header{ - Name: "metadata.json", - Size: int64(len(metadataContent)), - Mode: 0644, - ModTime: time.Now(), - } - if err := tw.WriteHeader(metadataHeader); err != nil { - t.Fatalf("failed to write header: %v", err) + bundle := Bundle{ + BaseCRL: base, } - tw.Write(metadataContent) - tw.Close() - - _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") + if err := bundle.Validate(); err.Error() != "base CRL URL is missing" { + t.Fatalf("expected base CRL URL is missing, got %v", err) } }) - t.Run("missing metadata createAt", func(t *testing.T) { - var buf bytes.Buffer - tw := tar.NewWriter(&buf) - baseCRLHeader := &tar.Header{ - Name: "base.crl", - Size: int64(len(crlBytes)), - Mode: 0644, - ModTime: time.Now(), - } - if err := tw.WriteHeader(baseCRLHeader); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Write(crlBytes) - - metadataContent := []byte(`{"base.crl": {"url": "https://example.com/base.crl"}}`) - metadataHeader := &tar.Header{ - Name: "metadata.json", - Size: int64(len(metadataContent)), - Mode: 0644, - ModTime: time.Now(), - } - if err := tw.WriteHeader(metadataHeader); err != nil { - t.Fatalf("failed to write header: %v", err) + t.Run("missing metadata cachedAt", func(t *testing.T) { + bundle := Bundle{ + BaseCRL: base, + Metadata: Metadata{ + BaseCRL: CRLMetadata{ + URL: "http://example.com", + }, + }, } - tw.Write(metadataContent) - tw.Close() - - _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") + if err := bundle.Validate(); err.Error() != "base CRL CachedAt is missing" { + t.Fatalf("expected base CRL CachedAt is missing, got %v", err) } }) } diff --git a/revocation/crl/cache/file.go b/revocation/crl/cache/file.go deleted file mode 100644 index 5c47ea57..00000000 --- a/revocation/crl/cache/file.go +++ /dev/null @@ -1,248 +0,0 @@ -// 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 cache - -import ( - "archive/tar" - "context" - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "time" - - "github.com/notaryproject/notation-core-go/revocation/internal/file" -) - -const ( - // pathBaseCRL is the file name of the base CRL - pathBaseCRL = "base.crl" - - // pathMetadata is the file name of the metadata - pathMetadata = "metadata.json" - - // tempFileName is the prefix of the temporary file - tempFileName = "notation-*" -) - -// FileCache stores in a tarball format, which contains two files: base.crl and -// metadata.json. The base.crl file contains the base CRL in DER format, and the -// metadata.json file contains the metadata of the CRL. -// -// The cache builds on top of the UNIX file system to leverage the file system's -// atomic operations. The `rename` and `remove` operations will unlink the old -// file but keep the inode and file descriptor for existing processes to access -// the file. The old inode will be dereferenced when all processes close the old -// file descriptor. Additionally, the operations are proven to be atomic on -// UNIX-like platforms, so there is no need to handle file locking. -// -// NOTE: For Windows, the `open`, `rename` and `remove` operations need file -// locking to ensure atomicity. The current implementation does not handle -// file locking, so the concurrent write from multiple processes may be failed. -// Please do not use this cache in a multi-process environment on Windows. -// -// FileCache doesn't handle cache cleaning but provides the Delete and Clear -// methods to remove the CRLs from the file system. -type FileCache struct { - // MaxAge is the maximum age of the CRLs cache. If the CRL is older than - // MaxAge, it will be considered as expired. - MaxAge time.Duration - - root string -} - -// NewFileCache creates a new file system store -// -// - root is the directory to store the CRLs. -func NewFileCache(root string) (*FileCache, error) { - if err := os.MkdirAll(root, 0700); err != nil { - return nil, fmt.Errorf("failed to create directory: %w", err) - } - - return &FileCache{ - MaxAge: DefaultMaxAge, - root: root, - }, nil -} - -// Get retrieves the CRL bundle from the file system -// -// - if the key does not exist, return ErrNotFound -// - if the CRL is expired, return ErrCacheMiss -func (c *FileCache) Get(ctx context.Context, uri string) (bundle *Bundle, err error) { - f, err := os.Open(filepath.Join(c.root, fileName(uri))) - if err != nil { - if os.IsNotExist(err) { - return nil, ErrCacheMiss - } - return nil, err - } - defer f.Close() - - bundle, err = parseBundleFromTar(f) - if err != nil { - return nil, err - } - - expires := bundle.Metadata.CachedAt.Add(c.MaxAge) - if c.MaxAge > 0 && time.Now().After(expires) { - // do not delete the file to maintain the idempotent behavior - return nil, ErrCacheMiss - } - - return bundle, nil -} - -// Set stores the CRL bundle in the file system -func (c *FileCache) Set(ctx context.Context, uri string, bundle *Bundle) error { - if err := bundle.Validate(); err != nil { - return err - } - - // save to temp file - tempFile, err := os.CreateTemp("", tempFileName) - if err != nil { - return err - } - file.Using(tempFile, func(f *os.File) error { - return saveTar(tempFile, bundle) - }) - - // rename is atomic on UNIX-like platforms - return os.Rename(tempFile.Name(), filepath.Join(c.root, fileName(uri))) -} - -// fileName returns the file name of the CRL bundle tarball -func fileName(url string) string { - return hashURL(url) + ".tar" -} - -// hashURL hashes the URL with SHA256 and returns the hex-encoded result -func hashURL(url string) string { - hash := sha256.Sum256([]byte(url)) - return hex.EncodeToString(hash[:]) -} - -// parseBundleFromTar parses the CRL blob from a tarball -// -// The tarball should contain two files: -// - base.crl: the base CRL in DER format -// - metadata.json: the metadata of the CRL -// -// example of metadata.json: -// -// { -// "baseCRL": { -// "url": "https://example.com/base.crl", -// "nextUpdate": "2024-07-20T00:00:00Z" -// }, -// "cachedAt": "2024-07-20T00:00:00Z" -// } -func parseBundleFromTar(data io.Reader) (*Bundle, error) { - var bundle Bundle - - // parse the tarball - tar := tar.NewReader(data) - for { - header, err := tar.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, fmt.Errorf("failed to read tarball: %w", err) - } - - switch header.Name { - case pathBaseCRL: - // parse base.crl - data, err := io.ReadAll(tar) - if err != nil { - return nil, err - } - - baseCRL, err := x509.ParseRevocationList(data) - if err != nil { - return nil, fmt.Errorf("failed to parse base CRL from tarball: %w", err) - } - bundle.BaseCRL = baseCRL - case pathMetadata: - // parse metadata - var metadata Metadata - if err := json.NewDecoder(tar).Decode(&metadata); err != nil { - return nil, fmt.Errorf("failed to parse CRL metadata from tarball: %w", err) - } - bundle.Metadata = metadata - } - } - if err := bundle.Validate(); err != nil { - return nil, err - } - - return &bundle, nil -} - -// SaveAsTar saves the CRL blob as a tarball, including the base CRL and -// metadata -// -// The tarball should contain two files: -// - base.crl: the base CRL in DER format -// - metadata.json: the metadata of the CRL -// -// example of metadata.json: -// -// { -// "baseCRL": { -// "url": "https://example.com/base.crl", -// "nextUpdate": "2024-07-20T00:00:00Z" -// }, -// "cachedAt": "2024-06-30T00:00:00Z" -// } -func saveTar(w io.Writer, bundle *Bundle) (err error) { - tarWriter := tar.NewWriter(w) - defer func() { - if cerr := tarWriter.Close(); cerr != nil && err == nil { - err = cerr - } - }() - - // Add base.crl - if err := addToTar(pathBaseCRL, bundle.BaseCRL.Raw, bundle.Metadata.CachedAt, tarWriter); err != nil { - return err - } - - // Add metadata.json - metadataBytes, err := json.Marshal(bundle.Metadata) - if err != nil { - return err - } - return addToTar(pathMetadata, metadataBytes, bundle.Metadata.CachedAt, tarWriter) -} - -func addToTar(fileName string, data []byte, modTime time.Time, tw *tar.Writer) error { - header := &tar.Header{ - Name: fileName, - Size: int64(len(data)), - Mode: 0644, - ModTime: modTime, - } - if err := tw.WriteHeader(header); err != nil { - return err - } - _, err := tw.Write(data) - return err -} diff --git a/revocation/crl/cache/file_test.go b/revocation/crl/cache/file_test.go deleted file mode 100644 index 200f1261..00000000 --- a/revocation/crl/cache/file_test.go +++ /dev/null @@ -1,358 +0,0 @@ -// 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 cache - -import ( - "archive/tar" - "bytes" - "context" - "crypto/rand" - "crypto/x509" - "errors" - "math/big" - "os" - "path/filepath" - "reflect" - "runtime" - "strings" - "testing" - "time" - - "github.com/notaryproject/notation-core-go/testhelper" -) - -func TestFileCache(t *testing.T) { - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), - }, certChain[1].Cert, certChain[1].PrivateKey) - if err != nil { - t.Fatalf("failed to create base CRL: %v", err) - } - baseCRL, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatalf("failed to parse base CRL: %v", err) - } - - ctx := context.Background() - root := t.TempDir() - cache, err := NewFileCache(root) - t.Run("NewFileCache", func(t *testing.T) { - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - if cache.root != root { - t.Fatalf("expected dir %v, got %v", root, cache.root) - } - if cache.MaxAge != DefaultMaxAge { - t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.MaxAge) - } - }) - - key := "testKey" - bundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CachedAt: time.Now()}} - t.Run("SetAndGet comformance", func(t *testing.T) { - if err := cache.Set(ctx, key, bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - retrievedBundle, err := cache.Get(ctx, key) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if reflect.DeepEqual(bundle, retrievedBundle) { - t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) - } - }) - - t.Run("GetWithExpiredBundle", func(t *testing.T) { - expiredBundle := &Bundle{BaseCRL: baseCRL, Metadata: Metadata{BaseCRL: CRLMetadata{URL: "http://crl"}, CachedAt: time.Now().Add(-DefaultMaxAge - 1*time.Second)}} - if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = cache.Get(ctx, "expiredKey") - if !errors.Is(err, ErrCacheMiss) { - t.Fatalf("expected ErrCacheMiss, got %v", err) - } - }) - - t.Run("Cache interface", func(t *testing.T) { - var _ Cache = cache - }) -} - -func TestNewFileCache(t *testing.T) { - tempDir := t.TempDir() - t.Run("without permission to create cache directory", func(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("skipping test on Windows") - } - - if err := os.Chmod(tempDir, 0); err != nil { - t.Fatalf("failed to change permission: %v", err) - } - root := filepath.Join(tempDir, "test") - _, err := NewFileCache(root) - if err == nil { - t.Fatalf("expected error, got nil") - } - // restore permission - if err := os.Chmod(tempDir, 0755); err != nil { - t.Fatalf("failed to change permission: %v", err) - } - }) - - t.Run("no maxAge", func(t *testing.T) { - cache, err := NewFileCache(t.TempDir()) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - if cache.MaxAge != DefaultMaxAge { - t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.MaxAge) - } - }) -} - -func TestGetFailed(t *testing.T) { - tempDir := t.TempDir() - // write an invalid tarball - invalidTarball := filepath.Join(tempDir, "invalid.tar") - if err := os.WriteFile(invalidTarball, []byte("invalid tarball"), 0644); err != nil { - t.Fatalf("failed to write file: %v", err) - } - - cache, err := NewFileCache(tempDir) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - t.Run("invalid tarball", func(t *testing.T) { - _, err := cache.Get(context.Background(), "invalid.tar") - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("no permission to read file", func(t *testing.T) { - if err := os.Chmod(tempDir, 0); err != nil { - t.Fatalf("failed to change permission: %v", err) - } - _, err := cache.Get(context.Background(), "invalid.tar") - if err == nil { - t.Fatalf("expected error, got nil") - } - // restore permission - if err := os.Chmod(tempDir, 0755); err != nil { - t.Fatalf("failed to change permission: %v", err) - } - }) - - t.Run("invalid bundle file", func(t *testing.T) { - bundle := &Bundle{ - BaseCRL: &x509.RevocationList{Raw: []byte("invalid crl")}, - Metadata: Metadata{CachedAt: time.Now()}, - } - if err := saveTar(&bytes.Buffer{}, bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - if err := os.WriteFile(filepath.Join(tempDir, fileName("invalid")), []byte("invalid tarball"), 0644); err != nil { - t.Fatalf("failed to write file: %v", err) - } - _, err = cache.Get(context.Background(), "invalid") - if !strings.Contains(err.Error(), "failed to read tarball") { - t.Fatalf("expected error, got %v", err) - } - }) -} - -func TestSetFailed(t *testing.T) { - tempDir := t.TempDir() - cache, err := NewFileCache(tempDir) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - t.Run("failed to save tarball", func(t *testing.T) { - bundle := &Bundle{Metadata: Metadata{CachedAt: time.Now()}} - if err := cache.Set(context.Background(), "invalid.tar", bundle); err == nil { - t.Fatalf("expected error, got nil") - } - }) -} - -func TestParseAndSave(t *testing.T) { - const exampleURL = "https://example.com/base.crl" - var buf bytes.Buffer - - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), - }, certChain[1].Cert, certChain[1].PrivateKey) - if err != nil { - t.Fatalf("failed to create base CRL: %v", err) - } - t.Run("SaveAsTarball", func(t *testing.T) { - // Create a tarball - baseCRL, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatalf("failed to parse base CRL: %v", err) - } - bundle := &Bundle{ - BaseCRL: baseCRL, - Metadata: Metadata{ - BaseCRL: CRLMetadata{ - URL: exampleURL, - }, - CachedAt: time.Now(), - }, - } - - if err := saveTar(&buf, bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - }) - - t.Run("ParseBundleFromTarball", func(t *testing.T) { - // Parse the tarball - bundle, err := parseBundleFromTar(&buf) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if !bytes.Equal(crlBytes, bundle.BaseCRL.Raw) { - t.Errorf("expected BaseCRL to be %v, got %v", crlBytes, bundle.BaseCRL.Raw) - } - - if bundle.Metadata.BaseCRL.URL != exampleURL { - t.Errorf("expected URL to be %s, got %s", exampleURL, bundle.Metadata.BaseCRL.URL) - } - - if bundle.Metadata.CachedAt.IsZero() { - t.Errorf("expected CreateAt to be set, got zero value") - } - }) -} - -func TestBundleParseFailed(t *testing.T) { - t.Run("IO read error", func(t *testing.T) { - _, err := parseBundleFromTar(&errorReader{}) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("missing baseCRL content (only has baseCRL header in tarball)", func(t *testing.T) { - var buf bytes.Buffer - header := &tar.Header{ - Name: "base.crl", - Size: 10, - Mode: 0644, - ModTime: time.Now(), - } - tw := tar.NewWriter(&buf) - if err := tw.WriteHeader(header); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Close() - - _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("broken baseCRL", func(t *testing.T) { - var buf bytes.Buffer - header := &tar.Header{ - Name: "base.crl", - Size: 10, - Mode: 0644, - ModTime: time.Now(), - } - tw := tar.NewWriter(&buf) - if err := tw.WriteHeader(header); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Write([]byte("broken crl")) - tw.Close() - - _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("malformed metadata", func(t *testing.T) { - var buf bytes.Buffer - header := &tar.Header{ - Name: "metadata.json", - Size: 10, - Mode: 0644, - ModTime: time.Now(), - } - tw := tar.NewWriter(&buf) - if err := tw.WriteHeader(header); err != nil { - t.Fatalf("failed to write header: %v", err) - } - tw.Write([]byte("malformed json")) - tw.Close() - - _, err := parseBundleFromTar(bytes.NewReader(buf.Bytes())) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) -} - -func TestSaveTarFailed(t *testing.T) { - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), - }, certChain[1].Cert, certChain[1].PrivateKey) - if err != nil { - t.Fatalf("failed to create base CRL: %v", err) - } - - t.Run("write base CRL to tarball failed", func(t *testing.T) { - crl, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatalf("failed to parse base CRL: %v", err) - } - bundle := &Bundle{ - BaseCRL: crl, - Metadata: Metadata{ - BaseCRL: CRLMetadata{ - URL: "https://example.com/base.crl", - }, - CachedAt: time.Now(), - }, - } - if err := saveTar(&errorWriter{}, bundle); err == nil { - t.Fatalf("expected error, got nil") - } - }) -} - -type errorReader struct{} - -func (r *errorReader) Read(p []byte) (n int, err error) { - return 0, os.ErrNotExist -} - -type errorWriter struct { -} - -func (w *errorWriter) Write(p []byte) (n int, err error) { - return 0, os.ErrNotExist -} diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 8fe05eb0..2c9a5612 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -26,6 +26,7 @@ import ( "time" "github.com/notaryproject/notation-core-go/revocation/crl/cache" + "github.com/notaryproject/notation-core-go/revocation/internal/cachehelper" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -37,7 +38,7 @@ func TestNewHTTPFetcher(t *testing.T) { func TestFetch(t *testing.T) { // prepare cache - c := cache.NewMemoryCache() + c := cachehelper.NewMemoryCache() // prepare crl certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) @@ -121,9 +122,7 @@ func TestFetch(t *testing.T) { httpClient := &http.Client{ Transport: errorRoundTripperMock{}, } - newCache := cache.NewMemoryCache() f := NewHTTPFetcher(httpClient) - f.Cache = newCache _, _, err = f.Fetch(context.Background(), uncachedURL) if err == nil { t.Errorf("Fetcher.Fetch() error = nil, want not nil") diff --git a/revocation/crl/cache/memory.go b/revocation/internal/cachehelper/memory.go similarity index 85% rename from revocation/crl/cache/memory.go rename to revocation/internal/cachehelper/memory.go index def56b99..f1d13f7f 100644 --- a/revocation/crl/cache/memory.go +++ b/revocation/internal/cachehelper/memory.go @@ -11,13 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cache +package cachehelper import ( "context" "fmt" "sync" "time" + + "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) // MemoryCache is an in-memory cache that stores CRL bundles. @@ -39,7 +41,7 @@ type MemoryCache struct { // NewMemoryCache creates a new memory store. func NewMemoryCache() *MemoryCache { return &MemoryCache{ - MaxAge: DefaultMaxAge, + MaxAge: cache.DefaultMaxAge, } } @@ -47,26 +49,26 @@ func NewMemoryCache() *MemoryCache { // // - if the key does not exist, return ErrNotFound // - if the CRL is expired, return ErrCacheMiss -func (c *MemoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { +func (c *MemoryCache) Get(ctx context.Context, uri string) (*cache.Bundle, error) { value, ok := c.store.Load(uri) if !ok { - return nil, ErrCacheMiss + return nil, cache.ErrCacheMiss } - bundle, ok := value.(*Bundle) + bundle, ok := value.(*cache.Bundle) if !ok { return nil, fmt.Errorf("invalid type: %T", value) } expires := bundle.Metadata.CachedAt.Add(c.MaxAge) if c.MaxAge > 0 && time.Now().After(expires) { - return nil, ErrCacheMiss + return nil, cache.ErrCacheMiss } return bundle, nil } // Set stores the CRL in the memory store. -func (c *MemoryCache) Set(ctx context.Context, uri string, bundle *Bundle) error { +func (c *MemoryCache) Set(ctx context.Context, uri string, bundle *cache.Bundle) error { if err := bundle.Validate(); err != nil { return err } diff --git a/revocation/crl/cache/memory_test.go b/revocation/internal/cachehelper/memory_test.go similarity index 72% rename from revocation/crl/cache/memory_test.go rename to revocation/internal/cachehelper/memory_test.go index 311d36be..971aaa04 100644 --- a/revocation/crl/cache/memory_test.go +++ b/revocation/internal/cachehelper/memory_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cache +package cachehelper import ( "context" @@ -23,6 +23,7 @@ import ( "testing" "time" + "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -42,25 +43,25 @@ func TestMemoryCache(t *testing.T) { } // Test NewMemoryCache - cache := NewMemoryCache() - if cache.MaxAge != DefaultMaxAge { - t.Fatalf("expected maxAge %v, got %v", DefaultMaxAge, cache.MaxAge) + c := NewMemoryCache() + if c.MaxAge != cache.DefaultMaxAge { + t.Fatalf("expected maxAge %v, got %v", cache.DefaultMaxAge, c.MaxAge) } - bundle := &Bundle{ + bundle := &cache.Bundle{ BaseCRL: baseCRL, - Metadata: Metadata{ + Metadata: cache.Metadata{ CachedAt: time.Now(), - BaseCRL: CRLMetadata{ + BaseCRL: cache.CRLMetadata{ URL: "http://crl", }, }} key := "testKey" t.Run("SetAndGet comformance test", func(t *testing.T) { - if err := cache.Set(ctx, key, bundle); err != nil { + if err := c.Set(ctx, key, bundle); err != nil { t.Fatalf("expected no error, got %v", err) } - retrievedBundle, err := cache.Get(ctx, key) + retrievedBundle, err := c.Get(ctx, key) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -70,32 +71,32 @@ func TestMemoryCache(t *testing.T) { }) t.Run("GetWithExpiredBundle", func(t *testing.T) { - expiredBundle := &Bundle{ + expiredBundle := &cache.Bundle{ BaseCRL: baseCRL, - Metadata: Metadata{ - CachedAt: time.Now().Add(-DefaultMaxAge - 1*time.Second), - BaseCRL: CRLMetadata{ + Metadata: cache.Metadata{ + CachedAt: time.Now().Add(-cache.DefaultMaxAge - 1*time.Second), + BaseCRL: cache.CRLMetadata{ URL: "http://crl", }, }} - if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil { + if err := c.Set(ctx, "expiredKey", expiredBundle); err != nil { t.Fatalf("expected no error, got %v", err) } - _, err = cache.Get(ctx, "expiredKey") - if !errors.Is(err, ErrCacheMiss) { + _, err = c.Get(ctx, "expiredKey") + if !errors.Is(err, cache.ErrCacheMiss) { t.Fatalf("expected ErrCacheMiss, got %v", err) } }) t.Run("Key doesn't exist", func(t *testing.T) { - _, err := cache.Get(ctx, "nonExistentKey") - if !errors.Is(err, ErrCacheMiss) { + _, err := c.Get(ctx, "nonExistentKey") + if !errors.Is(err, cache.ErrCacheMiss) { t.Fatalf("expected ErrCacheMiss, got %v", err) } }) t.Run("Cache interface", func(t *testing.T) { - var _ Cache = cache + var _ cache.Cache = c }) } @@ -113,16 +114,16 @@ func TestMemoryCacheFailed(t *testing.T) { }) t.Run("ValidateFailed", func(t *testing.T) { - cache := NewMemoryCache() - bundle := &Bundle{ + c := NewMemoryCache() + bundle := &cache.Bundle{ BaseCRL: nil, - Metadata: Metadata{ + Metadata: cache.Metadata{ CachedAt: time.Now(), - BaseCRL: CRLMetadata{ + BaseCRL: cache.CRLMetadata{ URL: "http://crl", }, }} - err := cache.Set(ctx, "invalidBundle", bundle) + err := c.Set(ctx, "invalidBundle", bundle) if err == nil { t.Fatalf("expected error, got nil") } diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 9390ca32..c1bb6653 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -31,6 +31,7 @@ import ( crlutils "github.com/notaryproject/notation-core-go/revocation/crl" "github.com/notaryproject/notation-core-go/revocation/crl/cache" + "github.com/notaryproject/notation-core-go/revocation/internal/cachehelper" "github.com/notaryproject/notation-core-go/revocation/result" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -55,7 +56,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("download error", func(t *testing.T) { - memoryCache := cache.NewMemoryCache() + memoryCache := cachehelper.NewMemoryCache() cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, @@ -75,7 +76,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL validate failed", func(t *testing.T) { - memoryCache := cache.NewMemoryCache() + memoryCache := cachehelper.NewMemoryCache() cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, @@ -99,7 +100,7 @@ func TestCertCheckStatus(t *testing.T) { issuerKey := chain[1].PrivateKey t.Run("revoked", func(t *testing.T) { - memoryCache := cache.NewMemoryCache() + memoryCache := cachehelper.NewMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -128,7 +129,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("unknown critical extension", func(t *testing.T) { - memoryCache := cache.NewMemoryCache() + memoryCache := cachehelper.NewMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -163,7 +164,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("Not revoked", func(t *testing.T) { - memoryCache := cache.NewMemoryCache() + memoryCache := cachehelper.NewMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -186,7 +187,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL with delta CRL is not checked", func(t *testing.T) { - memoryCache := cache.NewMemoryCache() + memoryCache := cachehelper.NewMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -213,7 +214,7 @@ func TestCertCheckStatus(t *testing.T) { } }) - memoryCache := cache.NewMemoryCache() + memoryCache := cachehelper.NewMemoryCache() // create a stale CRL crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ diff --git a/revocation/revocation.go b/revocation/revocation.go index 88e0aaaa..414b680e 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -89,7 +89,8 @@ func New(httpClient *http.Client) (Revocation, error) { ocspHTTPClient: httpClient, crlHTTPClient: httpClient, certChainPurpose: purpose.CodeSigning, - crlCache: cache.NewMemoryCache(), + // no cache by default + crlCache: nil, }, nil } From db796ba026a74a9d52e58da7a03111ff6b2a941f Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 01:56:16 +0000 Subject: [PATCH 087/110] fix: remove Using function Signed-off-by: Junjie Gao --- revocation/internal/file/utils.go | 28 --------------- revocation/internal/file/utils_test.go | 47 -------------------------- 2 files changed, 75 deletions(-) delete mode 100644 revocation/internal/file/utils.go delete mode 100644 revocation/internal/file/utils_test.go diff --git a/revocation/internal/file/utils.go b/revocation/internal/file/utils.go deleted file mode 100644 index 0a374239..00000000 --- a/revocation/internal/file/utils.go +++ /dev/null @@ -1,28 +0,0 @@ -// 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 file provides utilities for file operations. -package file - -import "io" - -// Using is a helper function to ensure that a resource is closed after using it -// and return the error if any. -func Using[T io.Closer](t T, f func(t T) error) (err error) { - defer func() { - if closeErr := t.Close(); closeErr != nil && err == nil { - err = closeErr - } - }() - return f(t) -} diff --git a/revocation/internal/file/utils_test.go b/revocation/internal/file/utils_test.go deleted file mode 100644 index abf786dd..00000000 --- a/revocation/internal/file/utils_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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 file - -import ( - "errors" - "testing" -) - -func TestUsing(t *testing.T) { - t.Run("Close error", func(t *testing.T) { - err := Using(&mockCloser{err: errors.New("closer error")}, func(t *mockCloser) error { - return nil - }) - if err.Error() != "closer error" { - t.Fatalf("expected closer error, got %v", err) - } - }) - - t.Run("Close without error", func(t *testing.T) { - err := Using(&mockCloser{}, func(t *mockCloser) error { - return nil - }) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - }) -} - -type mockCloser struct { - err error -} - -func (m *mockCloser) Close() error { - return m.err -} From 7572b7fc210abb9360331b7e2e38731143bce45d Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 01:57:46 +0000 Subject: [PATCH 088/110] fix: update comment Signed-off-by: Junjie Gao --- revocation/revocation.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/revocation/revocation.go b/revocation/revocation.go index 414b680e..12b775d3 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -113,10 +113,6 @@ type Options struct { // CRLCache is the cache client used to store the CRL. if not provided, // no cache will be used. - // - // The cache package provides built-in cache implementations: - // - cache.NewMemoryCache: in-memory cache - // - cache.NewFileCache: file-based cache CRLCache cache.Cache } From fa816d2332cfd02832c3ae1f8795b989e392f0cc Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 03:44:04 +0000 Subject: [PATCH 089/110] fix: remove max age Signed-off-by: Junjie Gao --- revocation/crl/bundle.go | 38 +++++ revocation/crl/bundle_test.go | 27 ++++ revocation/crl/cache.go | 31 +++++ revocation/crl/cache/bundle.go | 73 ---------- revocation/crl/cache/bundle_test.go | 67 --------- revocation/crl/cache/cache.go | 56 -------- revocation/crl/{cache => }/errors.go | 2 +- revocation/crl/fetcher.go | 23 ++- revocation/crl/fetcher_test.go | 90 ++++++------ revocation/internal/cachehelper/memory.go | 78 ----------- .../internal/cachehelper/memory_test.go | 131 ------------------ revocation/internal/crl/crl.go | 6 +- revocation/internal/crl/crl_test.go | 68 ++++++--- revocation/revocation.go | 5 +- 14 files changed, 199 insertions(+), 496 deletions(-) create mode 100644 revocation/crl/bundle.go create mode 100644 revocation/crl/bundle_test.go create mode 100644 revocation/crl/cache.go delete mode 100644 revocation/crl/cache/bundle.go delete mode 100644 revocation/crl/cache/bundle_test.go delete mode 100644 revocation/crl/cache/cache.go rename revocation/crl/{cache => }/errors.go (98%) delete mode 100644 revocation/internal/cachehelper/memory.go delete mode 100644 revocation/internal/cachehelper/memory_test.go diff --git a/revocation/crl/bundle.go b/revocation/crl/bundle.go new file mode 100644 index 00000000..613574eb --- /dev/null +++ b/revocation/crl/bundle.go @@ -0,0 +1,38 @@ +// 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 crl + +import ( + "crypto/x509" + "errors" +) + +// Bundle is in memory representation of the Bundle tarball, including base CRL +// file and metadata file, which may be cached in the file system or other +// storage +// +// TODO: consider adding DeltaCRL field in the future +type Bundle struct { + // BaseCRL is the parsed base CRL + BaseCRL *x509.RevocationList +} + +// Validate checks if the bundle is valid +func (b *Bundle) Validate() error { + if b.BaseCRL == nil { + return errors.New("base CRL is missing") + } + + return nil +} diff --git a/revocation/crl/bundle_test.go b/revocation/crl/bundle_test.go new file mode 100644 index 00000000..40998683 --- /dev/null +++ b/revocation/crl/bundle_test.go @@ -0,0 +1,27 @@ +// 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 crl + +import ( + "testing" +) + +func TestValidate(t *testing.T) { + t.Run("missing BaseCRL", func(t *testing.T) { + var bundle Bundle + if err := bundle.Validate(); err.Error() != "base CRL is missing" { + t.Fatalf("expected base CRL is missing, got %v", err) + } + }) +} diff --git a/revocation/crl/cache.go b/revocation/crl/cache.go new file mode 100644 index 00000000..a5483419 --- /dev/null +++ b/revocation/crl/cache.go @@ -0,0 +1,31 @@ +// 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 crl + +import ( + "context" +) + +// Cache is an interface that specifies methods used for caching +type Cache interface { + // Get retrieves the CRL bundle with the given uri + // + // uri is the URI of the CRL + // + // if the key does not exist or the content is expired, return ErrCacheMiss. + Get(ctx context.Context, uri string) (*Bundle, error) + + // Set stores the CRL bundle with the given uri + Set(ctx context.Context, uri string, bundle *Bundle) error +} diff --git a/revocation/crl/cache/bundle.go b/revocation/crl/cache/bundle.go deleted file mode 100644 index fe73274e..00000000 --- a/revocation/crl/cache/bundle.go +++ /dev/null @@ -1,73 +0,0 @@ -// 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 cache - -import ( - "crypto/x509" - "errors" - "time" -) - -// CRLMetadata stores the URL of the CRL -type CRLMetadata struct { - // URL stores the URL of the CRL - URL string `json:"url"` - - // NextUpdate stores the next update time of the CRL - // - // This field extracts the `NextUpdate` field from the CRL extensions, which - // is an optional field in the CRL, so it may be empty. - NextUpdate time.Time `json:"nextUpdate"` -} - -// Metadata stores the metadata infomation of the CRL -// -// TODO: consider adding DeltaCRL field in the future -type Metadata struct { - // BaseCRL stores the URL of the base CRL - BaseCRL CRLMetadata `json:"baseCRL"` - - // CachedAt stores the creation time of the CRL bundle. This is different - // from the `ThisUpdate` field in the CRL. The `ThisUpdate` field in the CRL - // is the time when the CRL was generated, while the `CachedAt` field is for - // caching purpose, indicating the start of cache effective period. - CachedAt time.Time `json:"cachedAt"` -} - -// Bundle is in memory representation of the Bundle tarball, including base CRL -// file and metadata file, which may be cached in the file system or other -// storage -// -// TODO: consider adding DeltaCRL field in the future -type Bundle struct { - // BaseCRL is the parsed base CRL - BaseCRL *x509.RevocationList - - // Metadata is the metadata of the CRL bundle - Metadata Metadata -} - -// Validate checks if the bundle is valid -func (b *Bundle) Validate() error { - if b.BaseCRL == nil { - return errors.New("base CRL is missing") - } - if b.Metadata.BaseCRL.URL == "" { - return errors.New("base CRL URL is missing") - } - if b.Metadata.CachedAt.IsZero() { - return errors.New("base CRL CachedAt is missing") - } - return nil -} diff --git a/revocation/crl/cache/bundle_test.go b/revocation/crl/cache/bundle_test.go deleted file mode 100644 index 5220887f..00000000 --- a/revocation/crl/cache/bundle_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// 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 cache - -import ( - "crypto/rand" - "crypto/x509" - "math/big" - "testing" - - "github.com/notaryproject/notation-core-go/testhelper" -) - -func TestValidate(t *testing.T) { - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), - }, certChain[1].Cert, certChain[1].PrivateKey) - if err != nil { - t.Fatalf("failed to create base CRL: %v", err) - } - base, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatalf("failed to parse base CRL: %v", err) - } - - t.Run("missing BaseCRL", func(t *testing.T) { - var bundle Bundle - if err := bundle.Validate(); err.Error() != "base CRL is missing" { - t.Fatalf("expected base CRL is missing, got %v", err) - } - }) - - t.Run("missing metadata baseCRL URL", func(t *testing.T) { - bundle := Bundle{ - BaseCRL: base, - } - if err := bundle.Validate(); err.Error() != "base CRL URL is missing" { - t.Fatalf("expected base CRL URL is missing, got %v", err) - } - }) - - t.Run("missing metadata cachedAt", func(t *testing.T) { - bundle := Bundle{ - BaseCRL: base, - Metadata: Metadata{ - BaseCRL: CRLMetadata{ - URL: "http://example.com", - }, - }, - } - if err := bundle.Validate(); err.Error() != "base CRL CachedAt is missing" { - t.Fatalf("expected base CRL CachedAt is missing, got %v", err) - } - }) -} diff --git a/revocation/crl/cache/cache.go b/revocation/crl/cache/cache.go deleted file mode 100644 index 4d95e92f..00000000 --- a/revocation/crl/cache/cache.go +++ /dev/null @@ -1,56 +0,0 @@ -// 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 cache provides methods for caching CRL -// -// The fileSystemCache is an implementation of the Cache interface that uses the -// file system to store CRLs. The file system cache is built on top of the OS -// file system to leverage the file system's concurrency control and atomicity. -// -// The CRL is stored in a tarball format, which contains two files: base.crl and -// metadata.json. The base.crl file contains the base CRL in DER format, and the -// metadata.json file contains the metadata of the CRL. -// -// To implement a new cache, you need to create a new struct that implements the -// Cache interface. -// -// > Note: Please ensure that the implementation supports *CRL as the -// type of value field to cache a CRL. -package cache - -import ( - "context" - "time" -) - -const ( - // DefaultMaxAge is the default maximum age of the CRLs cache. - // If the CRL is older than DefaultMaxAge, it will be considered as expired. - // - // reference: Baseline Requirements for Code-Signing Certificates - // 4.9.7 CRL issuance frequency: https://cabforum.org/uploads/Baseline-Requirements-for-the-Issuance-and-Management-of-Code-Signing.v3.9.pdf - DefaultMaxAge = 7 * 24 * time.Hour -) - -// Cache is an interface that specifies methods used for caching -type Cache interface { - // Get retrieves the CRL bundle with the given uri - // - // uri is the URI of the CRL - // - // if the key does not exist or the content is expired, return ErrCacheMiss. - Get(ctx context.Context, uri string) (*Bundle, error) - - // Set stores the CRL bundle with the given uri - Set(ctx context.Context, uri string, bundle *Bundle) error -} diff --git a/revocation/crl/cache/errors.go b/revocation/crl/errors.go similarity index 98% rename from revocation/crl/cache/errors.go rename to revocation/crl/errors.go index ad0e8d7d..2fdae9f5 100644 --- a/revocation/crl/cache/errors.go +++ b/revocation/crl/errors.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cache +package crl import "errors" diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 088406fe..576edff6 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package crl provides Fetcher interface and its implementation to fetch -// CRL from the given URL +// Package crl provides Fetcher and Cache interface and implementations for +// fetching CRLs. package crl import ( @@ -24,13 +24,12 @@ import ( "net/http" "net/url" "time" - - "github.com/notaryproject/notation-core-go/revocation/crl/cache" ) // MaxCRLSize is the maximum size of CRL in bytes // -// CRL examples: https://chasersystems.com/blog/an-analysis-of-certificate-revocation-list-sizes/ +// The 32 MiB limit is based on investigation that even the largest CRLs +// are less than 16 MiB. The limit is set to 32 MiB to prevent const MaxCRLSize = 32 * 1024 * 1024 // 32 MiB // Fetcher is an interface that specifies methods used for fetching CRL @@ -40,10 +39,11 @@ type Fetcher interface { Fetch(ctx context.Context, uri string) (base, delta *x509.RevocationList, err error) } +// HTTPFetcher is a Fetcher implementation that fetches CRL from the given URL type HTTPFetcher struct { // Cache stores fetched CRLs and reuses them with the max ages. // If Cache is nil, no cache is used. - Cache cache.Cache + Cache Cache httpClient *http.Client } @@ -80,7 +80,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, uri string) (base, delta *x509. } // check expiry - nextUpdate := bundle.Metadata.BaseCRL.NextUpdate + nextUpdate := bundle.BaseCRL.NextUpdate if !nextUpdate.IsZero() && time.Now().After(nextUpdate) { return f.download(ctx, uri) } @@ -101,15 +101,8 @@ func (f *HTTPFetcher) download(ctx context.Context, uri string) (base, delta *x5 return base, delta, nil } - bundle := &cache.Bundle{ + bundle := &Bundle{ BaseCRL: base, - Metadata: cache.Metadata{ - BaseCRL: cache.CRLMetadata{ - URL: uri, - NextUpdate: base.NextUpdate, - }, - CachedAt: time.Now(), - }, } // ignore the error, as the cache is not critical _ = f.Cache.Set(ctx, uri, bundle) diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 2c9a5612..01d02b7c 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -22,11 +22,9 @@ import ( "io" "math/big" "net/http" + "sync" "testing" - "time" - "github.com/notaryproject/notation-core-go/revocation/crl/cache" - "github.com/notaryproject/notation-core-go/revocation/internal/cachehelper" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -38,7 +36,7 @@ func TestNewHTTPFetcher(t *testing.T) { func TestFetch(t *testing.T) { // prepare cache - c := cachehelper.NewMemoryCache() + c := newMemoryCache() // prepare crl certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) @@ -55,14 +53,8 @@ func TestFetch(t *testing.T) { const exampleURL = "http://example.com" const uncachedURL = "http://uncached.com" - bundle := &cache.Bundle{ + bundle := &Bundle{ BaseCRL: baseCRL, - Metadata: cache.Metadata{ - BaseCRL: cache.CRLMetadata{ - URL: exampleURL, - }, - CachedAt: time.Now(), - }, } if err := c.Set(context.Background(), exampleURL, bundle); err != nil { t.Errorf("Cache.Set() error = %v, want nil", err) @@ -128,45 +120,6 @@ func TestFetch(t *testing.T) { t.Errorf("Fetcher.Fetch() error = nil, want not nil") } }) - - t.Run("cache expired", func(t *testing.T) { - expiredBundle := &cache.Bundle{ - BaseCRL: baseCRL, - Metadata: cache.Metadata{ - BaseCRL: cache.CRLMetadata{ - URL: exampleURL, - NextUpdate: time.Now().Add(-1 * time.Hour), - }, - CachedAt: time.Now().Add(-1 * time.Hour), - }, - } - if err := c.Set(context.Background(), exampleURL, expiredBundle); err != nil { - t.Errorf("Cache.Set() error = %v, want nil", err) - } - - // generate a new CRL - // prepare crl - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) - newCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), - }, certChain[1].Cert, certChain[1].PrivateKey) - if err != nil { - t.Fatalf("failed to create base CRL: %v", err) - } - - httpClient := &http.Client{ - Transport: expectedRoundTripperMock{Body: newCRLBytes}, - } - f := NewHTTPFetcher(httpClient) - f.Cache = c - base, _, err := f.Fetch(context.Background(), exampleURL) - if err != nil { - t.Errorf("Fetcher.Fetch() error = %v, want nil", err) - } - if !bytes.Equal(base.Raw, newCRLBytes) { - t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) - } - }) } func TestDownload(t *testing.T) { @@ -283,3 +236,40 @@ func (rt expectedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, Body: io.NopCloser(bytes.NewBuffer(rt.Body)), }, nil } + +// memoryCache is an in-memory cache that stores CRL bundles for testing. +type memoryCache struct { + store sync.Map +} + +// newMemoryCache creates a new memory store. +func newMemoryCache() *memoryCache { + return &memoryCache{} +} + +// Get retrieves the CRL from the memory store. +// +// - if the key does not exist, return ErrNotFound +// - if the CRL is expired, return ErrCacheMiss +func (c *memoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { + value, ok := c.store.Load(uri) + if !ok { + return nil, ErrCacheMiss + } + bundle, ok := value.(*Bundle) + if !ok { + return nil, fmt.Errorf("invalid type: %T", value) + } + + return bundle, nil +} + +// Set stores the CRL in the memory store. +func (c *memoryCache) Set(ctx context.Context, uri string, bundle *Bundle) error { + if err := bundle.Validate(); err != nil { + return err + } + + c.store.Store(uri, bundle) + return nil +} diff --git a/revocation/internal/cachehelper/memory.go b/revocation/internal/cachehelper/memory.go deleted file mode 100644 index f1d13f7f..00000000 --- a/revocation/internal/cachehelper/memory.go +++ /dev/null @@ -1,78 +0,0 @@ -// 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 cachehelper - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/notaryproject/notation-core-go/revocation/crl/cache" -) - -// MemoryCache is an in-memory cache that stores CRL bundles. -// -// The cache is built on top of the sync.Map to leverage the concurrency control -// and atomicity of the map, so it is suitable for writing once and reading many -// times. The CRL is stored in memory as a Bundle type. -// -// MemoryCache doesn't handle cache cleaning but provides the Delete and Clear -// methods to remove the CRLs from the memory. -type MemoryCache struct { - // MaxAge is the maximum age of the CRLs cache. If the CRL is older than - // MaxAge, it will be considered as expired. - MaxAge time.Duration - - store sync.Map -} - -// NewMemoryCache creates a new memory store. -func NewMemoryCache() *MemoryCache { - return &MemoryCache{ - MaxAge: cache.DefaultMaxAge, - } -} - -// Get retrieves the CRL from the memory store. -// -// - if the key does not exist, return ErrNotFound -// - if the CRL is expired, return ErrCacheMiss -func (c *MemoryCache) Get(ctx context.Context, uri string) (*cache.Bundle, error) { - value, ok := c.store.Load(uri) - if !ok { - return nil, cache.ErrCacheMiss - } - bundle, ok := value.(*cache.Bundle) - if !ok { - return nil, fmt.Errorf("invalid type: %T", value) - } - - expires := bundle.Metadata.CachedAt.Add(c.MaxAge) - if c.MaxAge > 0 && time.Now().After(expires) { - return nil, cache.ErrCacheMiss - } - - return bundle, nil -} - -// Set stores the CRL in the memory store. -func (c *MemoryCache) Set(ctx context.Context, uri string, bundle *cache.Bundle) error { - if err := bundle.Validate(); err != nil { - return err - } - - c.store.Store(uri, bundle) - return nil -} diff --git a/revocation/internal/cachehelper/memory_test.go b/revocation/internal/cachehelper/memory_test.go deleted file mode 100644 index 971aaa04..00000000 --- a/revocation/internal/cachehelper/memory_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// 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 cachehelper - -import ( - "context" - "crypto/rand" - "crypto/x509" - "errors" - "math/big" - "reflect" - "testing" - "time" - - "github.com/notaryproject/notation-core-go/revocation/crl/cache" - "github.com/notaryproject/notation-core-go/testhelper" -) - -func TestMemoryCache(t *testing.T) { - ctx := context.Background() - - certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) - crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), - }, certChain[1].Cert, certChain[1].PrivateKey) - if err != nil { - t.Fatalf("failed to create base CRL: %v", err) - } - baseCRL, err := x509.ParseRevocationList(crlBytes) - if err != nil { - t.Fatalf("failed to parse base CRL: %v", err) - } - - // Test NewMemoryCache - c := NewMemoryCache() - if c.MaxAge != cache.DefaultMaxAge { - t.Fatalf("expected maxAge %v, got %v", cache.DefaultMaxAge, c.MaxAge) - } - - bundle := &cache.Bundle{ - BaseCRL: baseCRL, - Metadata: cache.Metadata{ - CachedAt: time.Now(), - BaseCRL: cache.CRLMetadata{ - URL: "http://crl", - }, - }} - key := "testKey" - t.Run("SetAndGet comformance test", func(t *testing.T) { - if err := c.Set(ctx, key, bundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - retrievedBundle, err := c.Get(ctx, key) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - if !reflect.DeepEqual(bundle, retrievedBundle) { - t.Fatalf("expected bundle %v, got %v", bundle, retrievedBundle) - } - }) - - t.Run("GetWithExpiredBundle", func(t *testing.T) { - expiredBundle := &cache.Bundle{ - BaseCRL: baseCRL, - Metadata: cache.Metadata{ - CachedAt: time.Now().Add(-cache.DefaultMaxAge - 1*time.Second), - BaseCRL: cache.CRLMetadata{ - URL: "http://crl", - }, - }} - if err := c.Set(ctx, "expiredKey", expiredBundle); err != nil { - t.Fatalf("expected no error, got %v", err) - } - _, err = c.Get(ctx, "expiredKey") - if !errors.Is(err, cache.ErrCacheMiss) { - t.Fatalf("expected ErrCacheMiss, got %v", err) - } - }) - - t.Run("Key doesn't exist", func(t *testing.T) { - _, err := c.Get(ctx, "nonExistentKey") - if !errors.Is(err, cache.ErrCacheMiss) { - t.Fatalf("expected ErrCacheMiss, got %v", err) - } - }) - - t.Run("Cache interface", func(t *testing.T) { - var _ cache.Cache = c - }) -} - -func TestMemoryCacheFailed(t *testing.T) { - ctx := context.Background() - - // Test Get with invalid type - t.Run("GetWithInvalidType", func(t *testing.T) { - cache := NewMemoryCache() - cache.store.Store("invalidKey", "invalidValue") - _, err := cache.Get(ctx, "invalidKey") - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - - t.Run("ValidateFailed", func(t *testing.T) { - c := NewMemoryCache() - bundle := &cache.Bundle{ - BaseCRL: nil, - Metadata: cache.Metadata{ - CachedAt: time.Now(), - BaseCRL: cache.CRLMetadata{ - URL: "http://crl", - }, - }} - err := c.Set(ctx, "invalidBundle", bundle) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) -} diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index 72d89a4e..de5362a6 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -41,13 +41,13 @@ var ( oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} ) -// CertCheckStatusOptions specifies values that are needed to check CRL +// CertCheckStatusOptions specifies values that are needed to check CRL. type CertCheckStatusOptions struct { - // HTTPClient is the HTTP client used to download the CRL + // Fetcher is used to fetch the CRL from the CRL distribution points. Fetcher crl.Fetcher // SigningTime is used to compare with the invalidity date during revocation - // check + // check. SigningTime time.Time } diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index c1bb6653..a5658ff9 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -26,12 +26,11 @@ import ( "math/big" "net/http" "strings" + "sync" "testing" "time" crlutils "github.com/notaryproject/notation-core-go/revocation/crl" - "github.com/notaryproject/notation-core-go/revocation/crl/cache" - "github.com/notaryproject/notation-core-go/revocation/internal/cachehelper" "github.com/notaryproject/notation-core-go/revocation/result" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -56,7 +55,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("download error", func(t *testing.T) { - memoryCache := cachehelper.NewMemoryCache() + memoryCache := newMemoryCache() cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, @@ -76,7 +75,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL validate failed", func(t *testing.T) { - memoryCache := cachehelper.NewMemoryCache() + memoryCache := newMemoryCache() cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, @@ -100,7 +99,7 @@ func TestCertCheckStatus(t *testing.T) { issuerKey := chain[1].PrivateKey t.Run("revoked", func(t *testing.T) { - memoryCache := cachehelper.NewMemoryCache() + memoryCache := newMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -129,7 +128,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("unknown critical extension", func(t *testing.T) { - memoryCache := cachehelper.NewMemoryCache() + memoryCache := newMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -164,7 +163,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("Not revoked", func(t *testing.T) { - memoryCache := cachehelper.NewMemoryCache() + memoryCache := newMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -187,7 +186,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL with delta CRL is not checked", func(t *testing.T) { - memoryCache := cachehelper.NewMemoryCache() + memoryCache := newMemoryCache() crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -214,7 +213,7 @@ func TestCertCheckStatus(t *testing.T) { } }) - memoryCache := cachehelper.NewMemoryCache() + memoryCache := newMemoryCache() // create a stale CRL crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ @@ -224,20 +223,14 @@ func TestCertCheckStatus(t *testing.T) { if err != nil { t.Fatal(err) } - crl, err := x509.ParseRevocationList(crlBytes) + base, err := x509.ParseRevocationList(crlBytes) if err != nil { t.Fatal(err) } - bundle := &cache.Bundle{ - BaseCRL: crl, - Metadata: cache.Metadata{ - BaseCRL: cache.CRLMetadata{ - URL: "http://example.com", - NextUpdate: crl.NextUpdate, - }, - CachedAt: time.Now(), - }, + bundle := &crlutils.Bundle{ + BaseCRL: base, } + chain[0].Cert.CRLDistributionPoints = []string{"http://example.com"} t.Run("invalid stale CRL cache, and re-download failed", func(t *testing.T) { @@ -719,3 +712,40 @@ func (rt expectedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, Body: io.NopCloser(bytes.NewBuffer(rt.Body)), }, nil } + +// memoryCache is an in-memory cache that stores CRL bundles for testing. +type memoryCache struct { + store sync.Map +} + +// newMemoryCache creates a new memory store. +func newMemoryCache() *memoryCache { + return &memoryCache{} +} + +// Get retrieves the CRL from the memory store. +// +// - if the key does not exist, return ErrNotFound +// - if the CRL is expired, return ErrCacheMiss +func (c *memoryCache) Get(ctx context.Context, uri string) (*crlutils.Bundle, error) { + value, ok := c.store.Load(uri) + if !ok { + return nil, crlutils.ErrCacheMiss + } + bundle, ok := value.(*crlutils.Bundle) + if !ok { + return nil, fmt.Errorf("invalid type: %T", value) + } + + return bundle, nil +} + +// Set stores the CRL in the memory store. +func (c *memoryCache) Set(ctx context.Context, uri string, bundle *crlutils.Bundle) error { + if err := bundle.Validate(); err != nil { + return err + } + + c.store.Store(uri, bundle) + return nil +} diff --git a/revocation/revocation.go b/revocation/revocation.go index 12b775d3..bd50b0b3 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -25,7 +25,6 @@ import ( "time" crlutils "github.com/notaryproject/notation-core-go/revocation/crl" - "github.com/notaryproject/notation-core-go/revocation/crl/cache" "github.com/notaryproject/notation-core-go/revocation/internal/crl" "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" "github.com/notaryproject/notation-core-go/revocation/internal/x509util" @@ -73,7 +72,7 @@ type revocation struct { ocspHTTPClient *http.Client crlHTTPClient *http.Client certChainPurpose purpose.Purpose - crlCache cache.Cache + crlCache crlutils.Cache } // New constructs a revocation object for code signing certificate chain. @@ -113,7 +112,7 @@ type Options struct { // CRLCache is the cache client used to store the CRL. if not provided, // no cache will be used. - CRLCache cache.Cache + CRLCache crlutils.Cache } // NewWithOptions constructs a Validator with the specified options From 53e88a0764f89b908414b3172fcaeb1e83947bd4 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 03:53:48 +0000 Subject: [PATCH 090/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/bundle.go | 14 +----------- revocation/crl/bundle_test.go | 27 ----------------------- revocation/crl/cache.go | 4 ++-- revocation/crl/fetcher.go | 33 +++++++++++++++-------------- revocation/crl/fetcher_test.go | 4 ---- revocation/internal/crl/crl_test.go | 4 ---- 6 files changed, 20 insertions(+), 66 deletions(-) delete mode 100644 revocation/crl/bundle_test.go diff --git a/revocation/crl/bundle.go b/revocation/crl/bundle.go index 613574eb..75eba8cd 100644 --- a/revocation/crl/bundle.go +++ b/revocation/crl/bundle.go @@ -15,24 +15,12 @@ package crl import ( "crypto/x509" - "errors" ) -// Bundle is in memory representation of the Bundle tarball, including base CRL -// file and metadata file, which may be cached in the file system or other -// storage +// Bundle is in memory representation of the Bundle tarball, including base CRL. // // TODO: consider adding DeltaCRL field in the future type Bundle struct { // BaseCRL is the parsed base CRL BaseCRL *x509.RevocationList } - -// Validate checks if the bundle is valid -func (b *Bundle) Validate() error { - if b.BaseCRL == nil { - return errors.New("base CRL is missing") - } - - return nil -} diff --git a/revocation/crl/bundle_test.go b/revocation/crl/bundle_test.go deleted file mode 100644 index 40998683..00000000 --- a/revocation/crl/bundle_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// 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 crl - -import ( - "testing" -) - -func TestValidate(t *testing.T) { - t.Run("missing BaseCRL", func(t *testing.T) { - var bundle Bundle - if err := bundle.Validate(); err.Error() != "base CRL is missing" { - t.Fatalf("expected base CRL is missing, got %v", err) - } - }) -} diff --git a/revocation/crl/cache.go b/revocation/crl/cache.go index a5483419..8899ea20 100644 --- a/revocation/crl/cache.go +++ b/revocation/crl/cache.go @@ -24,8 +24,8 @@ type Cache interface { // uri is the URI of the CRL // // if the key does not exist or the content is expired, return ErrCacheMiss. - Get(ctx context.Context, uri string) (*Bundle, error) + Get(ctx context.Context, url string) (*Bundle, error) // Set stores the CRL bundle with the given uri - Set(ctx context.Context, uri string, bundle *Bundle) error + Set(ctx context.Context, url string, bundle *Bundle) error } diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 576edff6..0fced9e4 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -36,7 +36,7 @@ const MaxCRLSize = 32 * 1024 * 1024 // 32 MiB // from the given URL type Fetcher interface { // Fetch retrieves the CRL from the given URL. - Fetch(ctx context.Context, uri string) (base, delta *x509.RevocationList, err error) + Fetch(ctx context.Context, url string) (base, delta *x509.RevocationList, err error) } // HTTPFetcher is a Fetcher implementation that fetches CRL from the given URL @@ -48,6 +48,7 @@ type HTTPFetcher struct { httpClient *http.Client } +// NewHTTPFetcher creates a new HTTPFetcher with the given HTTP client func NewHTTPFetcher(httpClient *http.Client) *HTTPFetcher { if httpClient == nil { httpClient = http.DefaultClient @@ -60,29 +61,29 @@ func NewHTTPFetcher(httpClient *http.Client) *HTTPFetcher { // Fetch retrieves the CRL from the given URL // -// Steps: -// 1. Try to get from cache -// 2. If not exist or broken, download and save to cache -func (f *HTTPFetcher) Fetch(ctx context.Context, uri string) (base, delta *x509.RevocationList, err error) { - if uri == "" { +// It try to get the CRL from the cache first, if the cache is not nil or have +// an error (e.g. cache miss), it will download the CRL from the URL, then +// store it to the cache if the cache is not nil. +func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (base, delta *x509.RevocationList, err error) { + if url == "" { return nil, nil, errors.New("CRL URL is empty") } if f.Cache == nil { // no cache, download directly - return f.download(ctx, uri) + return f.download(ctx, url) } // try to get from cache - bundle, err := f.Cache.Get(ctx, uri) + bundle, err := f.Cache.Get(ctx, url) if err != nil { - return f.download(ctx, uri) + return f.download(ctx, url) } // check expiry nextUpdate := bundle.BaseCRL.NextUpdate if !nextUpdate.IsZero() && time.Now().After(nextUpdate) { - return f.download(ctx, uri) + return f.download(ctx, url) } return bundle.BaseCRL, nil, nil @@ -90,8 +91,8 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, uri string) (base, delta *x509. // Download downloads the CRL from the given URL and saves it to the // cache -func (f *HTTPFetcher) download(ctx context.Context, uri string) (base, delta *x509.RevocationList, err error) { - base, err = download(ctx, uri, f.httpClient) +func (f *HTTPFetcher) download(ctx context.Context, url string) (base, delta *x509.RevocationList, err error) { + base, err = download(ctx, url, f.httpClient) if err != nil { return nil, nil, err } @@ -105,14 +106,14 @@ func (f *HTTPFetcher) download(ctx context.Context, uri string) (base, delta *x5 BaseCRL: base, } // ignore the error, as the cache is not critical - _ = f.Cache.Set(ctx, uri, bundle) + _ = f.Cache.Set(ctx, url, bundle) return base, delta, nil } -func download(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { +func download(ctx context.Context, baseURL string, client *http.Client) (*x509.RevocationList, error) { // validate URL - parsedURL, err := url.Parse(crlURL) + parsedURL, err := url.Parse(baseURL) if err != nil { return nil, fmt.Errorf("invalid CRL URL: %w", err) } @@ -121,7 +122,7 @@ func download(ctx context.Context, crlURL string, client *http.Client) (*x509.Re } // download CRL - req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil) if err != nil { return nil, fmt.Errorf("failed to create CRL request: %w", err) } diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 01d02b7c..e098802a 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -266,10 +266,6 @@ func (c *memoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { // Set stores the CRL in the memory store. func (c *memoryCache) Set(ctx context.Context, uri string, bundle *Bundle) error { - if err := bundle.Validate(); err != nil { - return err - } - c.store.Store(uri, bundle) return nil } diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index a5658ff9..65aa6413 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -742,10 +742,6 @@ func (c *memoryCache) Get(ctx context.Context, uri string) (*crlutils.Bundle, er // Set stores the CRL in the memory store. func (c *memoryCache) Set(ctx context.Context, uri string, bundle *crlutils.Bundle) error { - if err := bundle.Validate(); err != nil { - return err - } - c.store.Store(uri, bundle) return nil } From d7371a6470a80a27739dd7c3f7cd427f125f51ab Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 04:03:31 +0000 Subject: [PATCH 091/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 0fced9e4..8efc5f8d 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -41,7 +41,7 @@ type Fetcher interface { // HTTPFetcher is a Fetcher implementation that fetches CRL from the given URL type HTTPFetcher struct { - // Cache stores fetched CRLs and reuses them with the max ages. + // Cache stores fetched CRLs and reuses them until the CRL expires. // If Cache is nil, no cache is used. Cache Cache From 2df6f2f40dcde1f0cbebe8f14a0068d0cfc3a9c3 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 06:09:05 +0000 Subject: [PATCH 092/110] fix: resolve comments Signed-off-by: Junjie Gao --- revocation/crl/bundle.go | 14 ++++---- revocation/crl/cache.go | 10 +++--- revocation/crl/fetcher.go | 55 +++++++++++++++++----------- revocation/crl/fetcher_test.go | 56 +++++++++++++++++++---------- revocation/internal/crl/crl.go | 8 ++--- revocation/internal/crl/crl_test.go | 51 +++++++++++++++++++------- revocation/internal/crl/errors.go | 22 ------------ revocation/revocation.go | 34 ++++++++++-------- 8 files changed, 146 insertions(+), 104 deletions(-) delete mode 100644 revocation/internal/crl/errors.go diff --git a/revocation/crl/bundle.go b/revocation/crl/bundle.go index 75eba8cd..0c4e439b 100644 --- a/revocation/crl/bundle.go +++ b/revocation/crl/bundle.go @@ -13,14 +13,16 @@ package crl -import ( - "crypto/x509" -) +import "crypto/x509" -// Bundle is in memory representation of the Bundle tarball, including base CRL. -// -// TODO: consider adding DeltaCRL field in the future +// Bundle is a collection of CRLs, including base and delta CRLs type Bundle struct { // BaseCRL is the parsed base CRL BaseCRL *x509.RevocationList + + // DeltaCRL is the parsed delta CRL + // + // TODO: support delta CRL + // It will always be nil until we support delta CRL + DeltaCRL *x509.RevocationList } diff --git a/revocation/crl/cache.go b/revocation/crl/cache.go index 8899ea20..45e6bd19 100644 --- a/revocation/crl/cache.go +++ b/revocation/crl/cache.go @@ -13,19 +13,17 @@ package crl -import ( - "context" -) +import "context" // Cache is an interface that specifies methods used for caching type Cache interface { - // Get retrieves the CRL bundle with the given uri + // Get retrieves the CRL bundle with the given url // - // uri is the URI of the CRL + // url is the URI of the CRL // // if the key does not exist or the content is expired, return ErrCacheMiss. Get(ctx context.Context, url string) (*Bundle, error) - // Set stores the CRL bundle with the given uri + // Set stores the CRL bundle with the given url Set(ctx context.Context, url string, bundle *Bundle) error } diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 8efc5f8d..8cbf11e1 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -18,6 +18,7 @@ package crl import ( "context" "crypto/x509" + "encoding/asn1" "errors" "fmt" "io" @@ -26,17 +27,21 @@ import ( "time" ) -// MaxCRLSize is the maximum size of CRL in bytes +// oidFreshestCRL is the object identifier for the distribution point +// for the delta CRL. (See RFC 5280, Section 5.2.6) +var oidFreshestCRL = asn1.ObjectIdentifier{2, 5, 29, 46} + +// maxCRLSize is the maximum size of CRL in bytes // // The 32 MiB limit is based on investigation that even the largest CRLs // are less than 16 MiB. The limit is set to 32 MiB to prevent -const MaxCRLSize = 32 * 1024 * 1024 // 32 MiB +const maxCRLSize = 32 * 1024 * 1024 // 32 MiB // Fetcher is an interface that specifies methods used for fetching CRL // from the given URL type Fetcher interface { // Fetch retrieves the CRL from the given URL. - Fetch(ctx context.Context, url string) (base, delta *x509.RevocationList, err error) + Fetch(ctx context.Context, url string) (bundle *Bundle, err error) } // HTTPFetcher is a Fetcher implementation that fetches CRL from the given URL @@ -49,14 +54,14 @@ type HTTPFetcher struct { } // NewHTTPFetcher creates a new HTTPFetcher with the given HTTP client -func NewHTTPFetcher(httpClient *http.Client) *HTTPFetcher { +func NewHTTPFetcher(httpClient *http.Client) (*HTTPFetcher, error) { if httpClient == nil { - httpClient = http.DefaultClient + return nil, errors.New("httpClient is nil") } return &HTTPFetcher{ httpClient: httpClient, - } + }, nil } // Fetch retrieves the CRL from the given URL @@ -64,9 +69,9 @@ func NewHTTPFetcher(httpClient *http.Client) *HTTPFetcher { // It try to get the CRL from the cache first, if the cache is not nil or have // an error (e.g. cache miss), it will download the CRL from the URL, then // store it to the cache if the cache is not nil. -func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (base, delta *x509.RevocationList, err error) { +func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (bundle *Bundle, err error) { if url == "" { - return nil, nil, errors.New("CRL URL is empty") + return nil, errors.New("CRL URL is empty") } if f.Cache == nil { @@ -75,7 +80,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (base, delta *x509. } // try to get from cache - bundle, err := f.Cache.Get(ctx, url) + bundle, err = f.Cache.Get(ctx, url) if err != nil { return f.download(ctx, url) } @@ -86,29 +91,37 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (base, delta *x509. return f.download(ctx, url) } - return bundle.BaseCRL, nil, nil + return bundle, nil } // Download downloads the CRL from the given URL and saves it to the // cache -func (f *HTTPFetcher) download(ctx context.Context, url string) (base, delta *x509.RevocationList, err error) { - base, err = download(ctx, url, f.httpClient) +func (f *HTTPFetcher) download(ctx context.Context, url string) (bundle *Bundle, err error) { + base, err := download(ctx, url, f.httpClient) if err != nil { - return nil, nil, err + return nil, err + } + // check deltaCRL + for _, ext := range base.Extensions { + if ext.Id.Equal(oidFreshestCRL) { + // TODO: support delta CRL + return nil, errors.New("delta CRL is not supported") + } + } + + bundle = &Bundle{ + BaseCRL: base, } if f.Cache == nil { // no cache, return directly - return base, delta, nil + return bundle, nil } - bundle := &Bundle{ - BaseCRL: base, - } // ignore the error, as the cache is not critical _ = f.Cache.Set(ctx, url, bundle) - return base, delta, nil + return bundle, nil } func download(ctx context.Context, baseURL string, client *http.Client) (*x509.RevocationList, error) { @@ -135,12 +148,12 @@ func download(ctx context.Context, baseURL string, client *http.Client) (*x509.R return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) } // read with size limit - data, err := io.ReadAll(io.LimitReader(resp.Body, MaxCRLSize)) + data, err := io.ReadAll(io.LimitReader(resp.Body, maxCRLSize)) if err != nil { return nil, fmt.Errorf("failed to read CRL response: %w", err) } - if len(data) == MaxCRLSize { - return nil, fmt.Errorf("CRL size exceeds the limit: %d", MaxCRLSize) + if len(data) == maxCRLSize { + return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize) } // parse CRL diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index e098802a..6cecf620 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -30,7 +30,10 @@ import ( func TestNewHTTPFetcher(t *testing.T) { t.Run("httpClient is nil", func(t *testing.T) { - _ = NewHTTPFetcher(nil) + _, err := NewHTTPFetcher(nil) + if err.Error() != "httpClient is nil" { + t.Errorf("NewHTTPFetcher() error = %v, want %v", err, "httpClient is nil") + } }) } @@ -61,9 +64,13 @@ func TestFetch(t *testing.T) { } t.Run("url is empty", func(t *testing.T) { - f := NewHTTPFetcher(nil) + httpClient := &http.Client{} + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } f.Cache = c - _, _, err = f.Fetch(context.Background(), "") + _, err = f.Fetch(context.Background(), "") if err == nil { t.Errorf("Fetcher.Fetch() error = nil, want not nil") } @@ -73,25 +80,32 @@ func TestFetch(t *testing.T) { httpClient := &http.Client{ Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, } - f := NewHTTPFetcher(httpClient) - base, _, err := f.Fetch(context.Background(), exampleURL) + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } + bundle, err := f.Fetch(context.Background(), exampleURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } - if !bytes.Equal(base.Raw, baseCRL.Raw) { - t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) + if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) } }) t.Run("cache hit", func(t *testing.T) { - f := NewHTTPFetcher(nil) + httpClient := &http.Client{} + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } f.Cache = c - base, _, err := f.Fetch(context.Background(), exampleURL) + bundle, err := f.Fetch(context.Background(), exampleURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } - if !bytes.Equal(base.Raw, baseCRL.Raw) { - t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) + if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) } }) @@ -99,14 +113,17 @@ func TestFetch(t *testing.T) { httpClient := &http.Client{ Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, } - f := NewHTTPFetcher(httpClient) + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } f.Cache = c - base, _, err := f.Fetch(context.Background(), uncachedURL) + bundle, err := f.Fetch(context.Background(), uncachedURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } - if !bytes.Equal(base.Raw, baseCRL.Raw) { - t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", base.Raw, baseCRL.Raw) + if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) } }) @@ -114,8 +131,11 @@ func TestFetch(t *testing.T) { httpClient := &http.Client{ Transport: errorRoundTripperMock{}, } - f := NewHTTPFetcher(httpClient) - _, _, err = f.Fetch(context.Background(), uncachedURL) + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } + _, err = f.Fetch(context.Background(), uncachedURL) if err == nil { t.Errorf("Fetcher.Fetch() error = nil, want not nil") } @@ -174,7 +194,7 @@ func TestDownload(t *testing.T) { t.Run("exceed the size limit", func(t *testing.T) { _, err := download(context.Background(), "http://example.com", &http.Client{ - Transport: expectedRoundTripperMock{Body: make([]byte, MaxCRLSize+1)}, + Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, }) if err == nil { t.Fatal("expected error") diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index de5362a6..2a945b5d 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -100,18 +100,18 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C // not a performance issue. for _, crlURL = range cert.CRLDistributionPoints { // ignore delta CRL as it is not implemented - base, _, err := opts.Fetcher.Fetch(ctx, crlURL) + bundle, err := opts.Fetcher.Fetch(ctx, crlURL) if err != nil { lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err) break } - if err = validate(base, issuer); err != nil { + if err = validate(bundle.BaseCRL, issuer); err != nil { lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err) break } - crlResult, err := checkRevocation(cert, base, opts.SigningTime, crlURL) + crlResult, err := checkRevocation(cert, bundle.BaseCRL, opts.SigningTime, crlURL) if err != nil { lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err) break @@ -168,7 +168,7 @@ func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { for _, ext := range crl.Extensions { switch { case ext.Id.Equal(oidFreshestCRL): - return ErrDeltaCRLNotSupported + return errors.New("delta CRL is not supported") case ext.Id.Equal(oidIssuingDistributionPoint): // IssuingDistributionPoint is a critical extension that identifies // the scope of the CRL. Since we will check all the CRL diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 65aa6413..5c137604 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -20,7 +20,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" - "errors" "fmt" "io" "math/big" @@ -60,9 +59,12 @@ func TestCertCheckStatus(t *testing.T) { cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: errorRoundTripperMock{}}, ) + if err != nil { + t.Fatal(err) + } fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{ @@ -80,9 +82,12 @@ func TestCertCheckStatus(t *testing.T) { cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: expiredCRLRoundTripperMock{}}, ) + if err != nil { + t.Fatal(err) + } fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{ @@ -115,9 +120,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, ) + if err != nil { + t.Fatal(err) + } fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, @@ -150,9 +158,13 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, ) + if err != nil { + t.Fatal(err) + } + fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, @@ -173,9 +185,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, ) + if err != nil { + t.Fatal(err) + } fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, @@ -201,15 +216,18 @@ func TestCertCheckStatus(t *testing.T) { if err != nil { t.Fatal(err) } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, ) + if err != nil { + t.Fatal(err) + } fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) - if !errors.Is(r.ServerResults[0].Error, ErrDeltaCRLNotSupported) { - t.Fatal("expected ErrDeltaCRLNotChecked") + if !strings.Contains(r.ServerResults[0].Error.Error(), "delta CRL is not supported") { + t.Fatalf("unexpected error, got %v, expected %v", r.ServerResults[0].Error, "delta CRL is not supported") } }) @@ -239,9 +257,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: errorRoundTripperMock{}}, ) + if err != nil { + t.Fatal(err) + } fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, @@ -257,9 +278,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, ) + if err != nil { + t.Fatal(err) + } fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, @@ -283,9 +307,12 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } - fetcher := crlutils.NewHTTPFetcher( + fetcher, err := crlutils.NewHTTPFetcher( &http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}}, ) + if err != nil { + t.Fatal(err) + } fetcher.Cache = memoryCache r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, diff --git a/revocation/internal/crl/errors.go b/revocation/internal/crl/errors.go deleted file mode 100644 index 37866551..00000000 --- a/revocation/internal/crl/errors.go +++ /dev/null @@ -1,22 +0,0 @@ -// 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 crl - -import "errors" - -var ( - // ErrDeltaCRLNotSupported is returned when the CRL contains a delta CRL but - // the delta CRL is not supported. - ErrDeltaCRLNotSupported = errors.New("delta CRL is not supported") -) diff --git a/revocation/revocation.go b/revocation/revocation.go index bd50b0b3..9c78699b 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -24,7 +24,7 @@ import ( "sync" "time" - crlutils "github.com/notaryproject/notation-core-go/revocation/crl" + crlutil "github.com/notaryproject/notation-core-go/revocation/crl" "github.com/notaryproject/notation-core-go/revocation/internal/crl" "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" "github.com/notaryproject/notation-core-go/revocation/internal/x509util" @@ -70,9 +70,8 @@ type Validator interface { // revocation is an internal struct used for revocation checking type revocation struct { ocspHTTPClient *http.Client - crlHTTPClient *http.Client certChainPurpose purpose.Purpose - crlCache crlutils.Cache + crlFetcher crlutil.Fetcher } // New constructs a revocation object for code signing certificate chain. @@ -83,13 +82,15 @@ func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } + fetcher, err := crlutil.NewHTTPFetcher(httpClient) + if err != nil { + return nil, err + } return &revocation{ ocspHTTPClient: httpClient, - crlHTTPClient: httpClient, certChainPurpose: purpose.CodeSigning, - // no cache by default - crlCache: nil, + crlFetcher: fetcher, }, nil } @@ -105,14 +106,14 @@ type Options struct { // OPTIONAL. CRLHTTPClient *http.Client + // CRLCache is the cache client used to store the CRL. if not provided, + // no cache will be used. + CRLCache crlutil.Cache + // CertChainPurpose is the purpose of the certificate chain. Supported // values are CodeSigning and Timestamping. Default value is CodeSigning. // OPTIONAL. CertChainPurpose purpose.Purpose - - // CRLCache is the cache client used to store the CRL. if not provided, - // no cache will be used. - CRLCache crlutils.Cache } // NewWithOptions constructs a Validator with the specified options @@ -131,11 +132,16 @@ func NewWithOptions(opts Options) (Validator, error) { return nil, fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose) } + fetcher, err := crlutil.NewHTTPFetcher(opts.CRLHTTPClient) + if err != nil { + return nil, err + } + fetcher.Cache = opts.CRLCache + return &revocation{ ocspHTTPClient: opts.OCSPHTTPClient, - crlHTTPClient: opts.CRLHTTPClient, certChainPurpose: opts.CertChainPurpose, - crlCache: opts.CRLCache, + crlFetcher: fetcher, }, nil } @@ -181,10 +187,8 @@ func (r *revocation) ValidateContext(ctx context.Context, validateContextOpts Va SigningTime: validateContextOpts.AuthenticSigningTime, } - fetcher := crlutils.NewHTTPFetcher(r.crlHTTPClient) - fetcher.Cache = r.crlCache crlOpts := crl.CertCheckStatusOptions{ - Fetcher: fetcher, + Fetcher: r.crlFetcher, SigningTime: validateContextOpts.AuthenticSigningTime, } From 6267143fdf4bf6368d4a4a8018139d5d005fb1b5 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 06:19:42 +0000 Subject: [PATCH 093/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 8cbf11e1..4a203c66 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -46,7 +46,8 @@ type Fetcher interface { // HTTPFetcher is a Fetcher implementation that fetches CRL from the given URL type HTTPFetcher struct { - // Cache stores fetched CRLs and reuses them until the CRL expires. + // Cache stores fetched CRLs and reuses them until the CRL reaches the + // NextUpdate time. // If Cache is nil, no cache is used. Cache Cache @@ -66,9 +67,9 @@ func NewHTTPFetcher(httpClient *http.Client) (*HTTPFetcher, error) { // Fetch retrieves the CRL from the given URL // -// It try to get the CRL from the cache first, if the cache is not nil or have -// an error (e.g. cache miss), it will download the CRL from the URL, then -// store it to the cache if the cache is not nil. +// If cache is not nil, try to get the CRL from the cache first. On failure +// (e.g. cache miss), it will download the CRL from the URL and store it to the +// cache. func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (bundle *Bundle, err error) { if url == "" { return nil, errors.New("CRL URL is empty") @@ -94,7 +95,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (bundle *Bundle, er return bundle, nil } -// Download downloads the CRL from the given URL and saves it to the +// download downloads the CRL from the given URL and saves it to the // cache func (f *HTTPFetcher) download(ctx context.Context, url string) (bundle *Bundle, err error) { base, err := download(ctx, url, f.httpClient) From 9f8c8ce05bfb445f82d57e8b9865f085996eebb2 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 07:13:08 +0000 Subject: [PATCH 094/110] test: improve test coverage Signed-off-by: Junjie Gao --- revocation/crl/errors.go | 10 ++ revocation/crl/errors_test.go | 27 ++++++ revocation/crl/fetcher.go | 23 ++++- revocation/crl/fetcher_test.go | 138 +++++++++++++++++++++++++--- revocation/internal/crl/crl.go | 8 +- revocation/internal/crl/crl_test.go | 48 +++++++--- 6 files changed, 221 insertions(+), 33 deletions(-) create mode 100644 revocation/crl/errors_test.go diff --git a/revocation/crl/errors.go b/revocation/crl/errors.go index 2fdae9f5..6de0be7d 100644 --- a/revocation/crl/errors.go +++ b/revocation/crl/errors.go @@ -17,3 +17,13 @@ import "errors" // ErrCacheMiss is an error type for when a cache miss occurs var ErrCacheMiss = errors.New("cache miss") + +// CacheError is an error type for cache errors. The cache error is not a +// critical error, the following operations can be performed normally. +type CacheError struct { + Err error +} + +func (e CacheError) Error() string { + return e.Err.Error() +} diff --git a/revocation/crl/errors_test.go b/revocation/crl/errors_test.go new file mode 100644 index 00000000..8533d7eb --- /dev/null +++ b/revocation/crl/errors_test.go @@ -0,0 +1,27 @@ +// 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 crl + +import ( + "errors" + "testing" +) + +func TestCacheError(t *testing.T) { + err := errors.New("error") + cacheErr := CacheError{Err: err} + if cacheErr.Error() != err.Error() { + t.Errorf("expected %s, got %s", err.Error(), cacheErr.Error()) + } +} diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 4a203c66..2e29edb0 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -81,9 +81,19 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (bundle *Bundle, er } // try to get from cache - bundle, err = f.Cache.Get(ctx, url) - if err != nil { - return f.download(ctx, url) + bundle, cacheError := f.Cache.Get(ctx, url) + if cacheError != nil { + bundle, err := f.download(ctx, url) + if err != nil { + var cacheError *CacheError + if errors.As(err, &cacheError) { + return bundle, cacheError + } + return nil, err + } + return bundle, &CacheError{ + Err: fmt.Errorf("failed to get CRL from cache: %w", cacheError), + } } // check expiry @@ -120,7 +130,12 @@ func (f *HTTPFetcher) download(ctx context.Context, url string) (bundle *Bundle, } // ignore the error, as the cache is not critical - _ = f.Cache.Set(ctx, url, bundle) + cacheError := f.Cache.Set(ctx, url, bundle) + if cacheError != nil { + return bundle, &CacheError{ + Err: fmt.Errorf("failed to set CRL to cache: %w", cacheError), + } + } return bundle, nil } diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 6cecf620..440f194f 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -18,12 +18,15 @@ import ( "context" "crypto/rand" "crypto/x509" + "crypto/x509/pkix" + "errors" "fmt" "io" "math/big" "net/http" "sync" "testing" + "time" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -38,9 +41,6 @@ func TestNewHTTPFetcher(t *testing.T) { } func TestFetch(t *testing.T) { - // prepare cache - c := newMemoryCache() - // prepare crl certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ @@ -59,11 +59,9 @@ func TestFetch(t *testing.T) { bundle := &Bundle{ BaseCRL: baseCRL, } - if err := c.Set(context.Background(), exampleURL, bundle); err != nil { - t.Errorf("Cache.Set() error = %v, want nil", err) - } t.Run("url is empty", func(t *testing.T) { + c := &memoryCache{} httpClient := &http.Client{} f, err := NewHTTPFetcher(httpClient) if err != nil { @@ -94,6 +92,12 @@ func TestFetch(t *testing.T) { }) t.Run("cache hit", func(t *testing.T) { + // set the cache + c := &memoryCache{} + if err := c.Set(context.Background(), exampleURL, bundle); err != nil { + t.Errorf("Cache.Set() error = %v, want nil", err) + } + httpClient := &http.Client{} f, err := NewHTTPFetcher(httpClient) if err != nil { @@ -109,7 +113,24 @@ func TestFetch(t *testing.T) { } }) + t.Run("cache miss and download failed error", func(t *testing.T) { + c := &memoryCache{} + httpClient := &http.Client{ + Transport: errorRoundTripperMock{}, + } + f, err := NewHTTPFetcher(httpClient) + f.Cache = c + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } + _, err = f.Fetch(context.Background(), uncachedURL) + if err == nil { + t.Errorf("Fetcher.Fetch() error = nil, want not nil") + } + }) + t.Run("cache miss", func(t *testing.T) { + c := &memoryCache{} httpClient := &http.Client{ Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, } @@ -118,26 +139,108 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c + var cacheError *CacheError bundle, err := f.Fetch(context.Background(), uncachedURL) + if !errors.As(err, &cacheError) { + t.Errorf("Fetcher.Fetch() error = %v, want CacheError", err) + } + if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) + } + }) + + t.Run("cache expired", func(t *testing.T) { + c := &memoryCache{} + // prepare an expired CRL + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + expiredCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + NextUpdate: time.Now().Add(-1 * time.Hour), + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + expiredCRL, err := x509.ParseRevocationList(expiredCRLBytes) + if err != nil { + t.Fatalf("failed to parse base CRL: %v", err) + } + // store the expired CRL + const expiredCRLURL = "http://example.com/expired" + bundle := &Bundle{ + BaseCRL: expiredCRL, + } + if err := c.Set(context.Background(), expiredCRLURL, bundle); err != nil { + t.Errorf("Cache.Set() error = %v, want nil", err) + } + + // fetch the expired CRL + httpClient := &http.Client{ + Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, + } + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } + f.Cache = c + bundle, err = f.Fetch(context.Background(), expiredCRLURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } + // should re-download the CRL if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) { t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) } }) - t.Run("cache miss and download failed error", func(t *testing.T) { + t.Run("delta CRL is not supported", func(t *testing.T) { + c := &memoryCache{} + // prepare a CRL with refresh CRL extension + certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) + expiredCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + Number: big.NewInt(1), + NextUpdate: time.Now().Add(-1 * time.Hour), + ExtraExtensions: []pkix.Extension{ + { + Id: oidFreshestCRL, + Value: []byte{0x01, 0x02, 0x03}, + }, + }, + }, certChain[1].Cert, certChain[1].PrivateKey) + if err != nil { + t.Fatalf("failed to create base CRL: %v", err) + } + httpClient := &http.Client{ - Transport: errorRoundTripperMock{}, + Transport: expectedRoundTripperMock{Body: expiredCRLBytes}, } f, err := NewHTTPFetcher(httpClient) if err != nil { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } + f.Cache = c _, err = f.Fetch(context.Background(), uncachedURL) - if err == nil { - t.Errorf("Fetcher.Fetch() error = nil, want not nil") + if err.Error() != "delta CRL is not supported" { + t.Errorf("Fetcher.Fetch() error = %v, want delta CRL is not supported", err) + } + }) + + t.Run("Set cache error", func(t *testing.T) { + c := &errorCache{} + httpClient := &http.Client{ + Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, + } + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } + f.Cache = c + var cacheError *CacheError + bundle, err = f.Fetch(context.Background(), exampleURL) + if !errors.As(err, &cacheError) { + t.Errorf("Fetcher.Fetch() error = %v, want CacheError", err) + } + if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) { + t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) } }) } @@ -262,11 +365,6 @@ type memoryCache struct { store sync.Map } -// newMemoryCache creates a new memory store. -func newMemoryCache() *memoryCache { - return &memoryCache{} -} - // Get retrieves the CRL from the memory store. // // - if the key does not exist, return ErrNotFound @@ -289,3 +387,13 @@ func (c *memoryCache) Set(ctx context.Context, uri string, bundle *Bundle) error c.store.Store(uri, bundle) return nil } + +type errorCache struct{} + +func (c *errorCache) Get(ctx context.Context, uri string) (*Bundle, error) { + return nil, fmt.Errorf("Get error") +} + +func (c *errorCache) Set(ctx context.Context, uri string, bundle *Bundle) error { + return fmt.Errorf("Set error") +} diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index 2a945b5d..840c24ad 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -99,9 +99,9 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C // point with one CRL URI, which will be cached, so checking all the URIs is // not a performance issue. for _, crlURL = range cert.CRLDistributionPoints { - // ignore delta CRL as it is not implemented + var cacheError *crl.CacheError bundle, err := opts.Fetcher.Fetch(ctx, crlURL) - if err != nil { + if err != nil && !errors.As(err, &cacheError) { lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err) break } @@ -116,6 +116,10 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err) break } + if crlResult.Error == nil && cacheError != nil { + // insert the cache error to the result + crlResult.Error = cacheError + } if crlResult.Result == result.ResultRevoked { return &result.CertRevocationResult{ Result: result.ResultRevoked, diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 5c137604..3422ef3b 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -54,7 +54,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("download error", func(t *testing.T) { - memoryCache := newMemoryCache() + memoryCache := &memoryCache{} cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, @@ -77,7 +77,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL validate failed", func(t *testing.T) { - memoryCache := newMemoryCache() + memoryCache := &memoryCache{} cert := &x509.Certificate{ CRLDistributionPoints: []string{"http://example.com"}, @@ -104,7 +104,7 @@ func TestCertCheckStatus(t *testing.T) { issuerKey := chain[1].PrivateKey t.Run("revoked", func(t *testing.T) { - memoryCache := newMemoryCache() + memoryCache := &memoryCache{} crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -136,7 +136,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("unknown critical extension", func(t *testing.T) { - memoryCache := newMemoryCache() + memoryCache := &memoryCache{} crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -175,7 +175,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("Not revoked", func(t *testing.T) { - memoryCache := newMemoryCache() + memoryCache := &memoryCache{} crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -201,7 +201,7 @@ func TestCertCheckStatus(t *testing.T) { }) t.Run("CRL with delta CRL is not checked", func(t *testing.T) { - memoryCache := newMemoryCache() + memoryCache := &memoryCache{} crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ NextUpdate: time.Now().Add(time.Hour), @@ -231,7 +231,7 @@ func TestCertCheckStatus(t *testing.T) { } }) - memoryCache := newMemoryCache() + memoryCache := &memoryCache{} // create a stale CRL crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ @@ -416,6 +416,35 @@ func TestValidate(t *testing.T) { t.Fatal(err) } }) + + t.Run("delta CRL is not supported", func(t *testing.T) { + chain := testhelper.GetRevokableRSAChainWithRevocations(1, false, true) + issuerCert := chain[0].Cert + issuerKey := chain[0].PrivateKey + + crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + NextUpdate: time.Now().Add(time.Hour), + Number: big.NewInt(20240720), + ExtraExtensions: []pkix.Extension{ + { + Id: oidFreshestCRL, + Critical: false, + }, + }, + }, issuerCert, issuerKey) + if err != nil { + t.Fatal(err) + } + + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + t.Fatal(err) + } + + if err := validate(crl, issuerCert); err.Error() != "delta CRL is not supported" { + t.Fatalf("got %v, expected delta CRL is not supported", err) + } + }) } func TestCheckRevocation(t *testing.T) { @@ -745,11 +774,6 @@ type memoryCache struct { store sync.Map } -// newMemoryCache creates a new memory store. -func newMemoryCache() *memoryCache { - return &memoryCache{} -} - // Get retrieves the CRL from the memory store. // // - if the key does not exist, return ErrNotFound From 4bd6d06c982c846c5cf94b2e2976827f7fc052bc Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 07:19:45 +0000 Subject: [PATCH 095/110] fix: update comment Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 1 - 1 file changed, 1 deletion(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 2e29edb0..f5c41de9 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -129,7 +129,6 @@ func (f *HTTPFetcher) download(ctx context.Context, url string) (bundle *Bundle, return bundle, nil } - // ignore the error, as the cache is not critical cacheError := f.Cache.Set(ctx, url, bundle) if cacheError != nil { return bundle, &CacheError{ From 4d19eb6fd01cebd003e36f836a21a319fb80ee60 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 07:25:46 +0000 Subject: [PATCH 096/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/cache.go | 5 ++++- revocation/crl/fetcher_test.go | 12 ++++++------ revocation/internal/crl/crl_test.go | 8 ++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/revocation/crl/cache.go b/revocation/crl/cache.go index 45e6bd19..0410dfed 100644 --- a/revocation/crl/cache.go +++ b/revocation/crl/cache.go @@ -19,11 +19,14 @@ import "context" type Cache interface { // Get retrieves the CRL bundle with the given url // - // url is the URI of the CRL + // url is the key to retrieve the CRL bundle // // if the key does not exist or the content is expired, return ErrCacheMiss. Get(ctx context.Context, url string) (*Bundle, error) // Set stores the CRL bundle with the given url + // + // url is the key to store the CRL bundle + // bundle is the CRL collections to store Set(ctx context.Context, url string, bundle *Bundle) error } diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 440f194f..886b5e84 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -369,8 +369,8 @@ type memoryCache struct { // // - if the key does not exist, return ErrNotFound // - if the CRL is expired, return ErrCacheMiss -func (c *memoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { - value, ok := c.store.Load(uri) +func (c *memoryCache) Get(ctx context.Context, url string) (*Bundle, error) { + value, ok := c.store.Load(url) if !ok { return nil, ErrCacheMiss } @@ -383,17 +383,17 @@ func (c *memoryCache) Get(ctx context.Context, uri string) (*Bundle, error) { } // Set stores the CRL in the memory store. -func (c *memoryCache) Set(ctx context.Context, uri string, bundle *Bundle) error { - c.store.Store(uri, bundle) +func (c *memoryCache) Set(ctx context.Context, url string, bundle *Bundle) error { + c.store.Store(url, bundle) return nil } type errorCache struct{} -func (c *errorCache) Get(ctx context.Context, uri string) (*Bundle, error) { +func (c *errorCache) Get(ctx context.Context, url string) (*Bundle, error) { return nil, fmt.Errorf("Get error") } -func (c *errorCache) Set(ctx context.Context, uri string, bundle *Bundle) error { +func (c *errorCache) Set(ctx context.Context, url string, bundle *Bundle) error { return fmt.Errorf("Set error") } diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 3422ef3b..55fb4010 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -778,8 +778,8 @@ type memoryCache struct { // // - if the key does not exist, return ErrNotFound // - if the CRL is expired, return ErrCacheMiss -func (c *memoryCache) Get(ctx context.Context, uri string) (*crlutils.Bundle, error) { - value, ok := c.store.Load(uri) +func (c *memoryCache) Get(ctx context.Context, url string) (*crlutils.Bundle, error) { + value, ok := c.store.Load(url) if !ok { return nil, crlutils.ErrCacheMiss } @@ -792,7 +792,7 @@ func (c *memoryCache) Get(ctx context.Context, uri string) (*crlutils.Bundle, er } // Set stores the CRL in the memory store. -func (c *memoryCache) Set(ctx context.Context, uri string, bundle *crlutils.Bundle) error { - c.store.Store(uri, bundle) +func (c *memoryCache) Set(ctx context.Context, url string, bundle *crlutils.Bundle) error { + c.store.Store(url, bundle) return nil } From 8356d24516acda0f01f288b3546f6f2448c48275 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 08:25:51 +0000 Subject: [PATCH 097/110] fix: resolve comments Signed-off-by: Junjie Gao --- revocation/crl/bundle.go | 2 +- revocation/crl/errors.go | 10 ---------- revocation/crl/errors_test.go | 27 --------------------------- revocation/crl/fetcher.go | 24 +++++------------------- revocation/crl/fetcher_test.go | 11 ++++------- revocation/internal/crl/crl.go | 8 ++------ 6 files changed, 12 insertions(+), 70 deletions(-) delete mode 100644 revocation/crl/errors_test.go diff --git a/revocation/crl/bundle.go b/revocation/crl/bundle.go index 0c4e439b..63b7e0f4 100644 --- a/revocation/crl/bundle.go +++ b/revocation/crl/bundle.go @@ -22,7 +22,7 @@ type Bundle struct { // DeltaCRL is the parsed delta CRL // - // TODO: support delta CRL + // TODO: support delta CRL https://github.com/notaryproject/notation-core-go/issues/228 // It will always be nil until we support delta CRL DeltaCRL *x509.RevocationList } diff --git a/revocation/crl/errors.go b/revocation/crl/errors.go index 6de0be7d..2fdae9f5 100644 --- a/revocation/crl/errors.go +++ b/revocation/crl/errors.go @@ -17,13 +17,3 @@ import "errors" // ErrCacheMiss is an error type for when a cache miss occurs var ErrCacheMiss = errors.New("cache miss") - -// CacheError is an error type for cache errors. The cache error is not a -// critical error, the following operations can be performed normally. -type CacheError struct { - Err error -} - -func (e CacheError) Error() string { - return e.Err.Error() -} diff --git a/revocation/crl/errors_test.go b/revocation/crl/errors_test.go deleted file mode 100644 index 8533d7eb..00000000 --- a/revocation/crl/errors_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// 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 crl - -import ( - "errors" - "testing" -) - -func TestCacheError(t *testing.T) { - err := errors.New("error") - cacheErr := CacheError{Err: err} - if cacheErr.Error() != err.Error() { - t.Errorf("expected %s, got %s", err.Error(), cacheErr.Error()) - } -} diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index f5c41de9..7a5cd15f 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -81,19 +81,10 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (bundle *Bundle, er } // try to get from cache - bundle, cacheError := f.Cache.Get(ctx, url) - if cacheError != nil { - bundle, err := f.download(ctx, url) - if err != nil { - var cacheError *CacheError - if errors.As(err, &cacheError) { - return bundle, cacheError - } - return nil, err - } - return bundle, &CacheError{ - Err: fmt.Errorf("failed to get CRL from cache: %w", cacheError), - } + bundle, err = f.Cache.Get(ctx, url) + if err != nil { + return f.download(ctx, url) + } // check expiry @@ -129,12 +120,7 @@ func (f *HTTPFetcher) download(ctx context.Context, url string) (bundle *Bundle, return bundle, nil } - cacheError := f.Cache.Set(ctx, url, bundle) - if cacheError != nil { - return bundle, &CacheError{ - Err: fmt.Errorf("failed to set CRL to cache: %w", cacheError), - } - } + _ = f.Cache.Set(ctx, url, bundle) return bundle, nil } diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 886b5e84..58249f65 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -19,7 +19,6 @@ import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" - "errors" "fmt" "io" "math/big" @@ -139,10 +138,9 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - var cacheError *CacheError bundle, err := f.Fetch(context.Background(), uncachedURL) - if !errors.As(err, &cacheError) { - t.Errorf("Fetcher.Fetch() error = %v, want CacheError", err) + if err != nil { + t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) { t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) @@ -234,10 +232,9 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - var cacheError *CacheError bundle, err = f.Fetch(context.Background(), exampleURL) - if !errors.As(err, &cacheError) { - t.Errorf("Fetcher.Fetch() error = %v, want CacheError", err) + if err != nil { + t.Errorf("Fetcher.Fetch() error = %v, want nil", err) } if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) { t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index 840c24ad..3ab0abd8 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -99,9 +99,8 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C // point with one CRL URI, which will be cached, so checking all the URIs is // not a performance issue. for _, crlURL = range cert.CRLDistributionPoints { - var cacheError *crl.CacheError bundle, err := opts.Fetcher.Fetch(ctx, crlURL) - if err != nil && !errors.As(err, &cacheError) { + if err != nil { lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err) break } @@ -116,10 +115,7 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err) break } - if crlResult.Error == nil && cacheError != nil { - // insert the cache error to the result - crlResult.Error = cacheError - } + if crlResult.Result == result.ResultRevoked { return &result.CertRevocationResult{ Result: result.ResultRevoked, From b466b4361da145bf766bfb1f3bfe9f46a2ca83d7 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 08:28:05 +0000 Subject: [PATCH 098/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 7a5cd15f..9caf71d1 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -83,6 +83,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (bundle *Bundle, er // try to get from cache bundle, err = f.Cache.Get(ctx, url) if err != nil { + // ignore cache error as it is not critical return f.download(ctx, url) } @@ -120,6 +121,7 @@ func (f *HTTPFetcher) download(ctx context.Context, url string) (bundle *Bundle, return bundle, nil } + // ignore the cache error as it is not critical _ = f.Cache.Set(ctx, url, bundle) return bundle, nil From 95e8590cff6e13494f0e7e3794b407b1d54456c7 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 08:31:27 +0000 Subject: [PATCH 099/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 4 ++-- revocation/crl/fetcher_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 9caf71d1..473f8198 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -57,7 +57,7 @@ type HTTPFetcher struct { // NewHTTPFetcher creates a new HTTPFetcher with the given HTTP client func NewHTTPFetcher(httpClient *http.Client) (*HTTPFetcher, error) { if httpClient == nil { - return nil, errors.New("httpClient is nil") + return nil, errors.New("httpClient cannot be nil") } return &HTTPFetcher{ @@ -72,7 +72,7 @@ func NewHTTPFetcher(httpClient *http.Client) (*HTTPFetcher, error) { // cache. func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (bundle *Bundle, err error) { if url == "" { - return nil, errors.New("CRL URL is empty") + return nil, errors.New("CRL URL cannot be empty") } if f.Cache == nil { diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 58249f65..6fa95600 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -33,8 +33,8 @@ import ( func TestNewHTTPFetcher(t *testing.T) { t.Run("httpClient is nil", func(t *testing.T) { _, err := NewHTTPFetcher(nil) - if err.Error() != "httpClient is nil" { - t.Errorf("NewHTTPFetcher() error = %v, want %v", err, "httpClient is nil") + if err.Error() != "httpClient cannot be nil" { + t.Errorf("NewHTTPFetcher() error = %v, want %v", err, "httpClient cannot be nil") } }) } @@ -68,8 +68,8 @@ func TestFetch(t *testing.T) { } f.Cache = c _, err = f.Fetch(context.Background(), "") - if err == nil { - t.Errorf("Fetcher.Fetch() error = nil, want not nil") + if err.Error() != "CRL URL cannot be empty" { + t.Fatalf("Fetcher.Fetch() error = %v, want CRL URL cannot be empty", err) } }) From d78d089fc75315b74af121bfef88702fced9c5aa Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 09:11:31 +0000 Subject: [PATCH 100/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 64 +++++++++++++++------------------- revocation/crl/fetcher_test.go | 22 ++++++------ revocation/internal/crl/crl.go | 1 + revocation/revocation.go | 6 ++-- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 473f8198..e65117a1 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -41,7 +41,7 @@ const maxCRLSize = 32 * 1024 * 1024 // 32 MiB // from the given URL type Fetcher interface { // Fetch retrieves the CRL from the given URL. - Fetch(ctx context.Context, url string) (bundle *Bundle, err error) + Fetch(ctx context.Context, url string) (*Bundle, error) } // HTTPFetcher is a Fetcher implementation that fetches CRL from the given URL @@ -70,66 +70,60 @@ func NewHTTPFetcher(httpClient *http.Client) (*HTTPFetcher, error) { // If cache is not nil, try to get the CRL from the cache first. On failure // (e.g. cache miss), it will download the CRL from the URL and store it to the // cache. -func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (bundle *Bundle, err error) { +func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { if url == "" { return nil, errors.New("CRL URL cannot be empty") } - if f.Cache == nil { - // no cache, download directly - return f.download(ctx, url) + if f.Cache != nil { + bundle, err := f.Cache.Get(ctx, url) + if err == nil { + // check expiry + nextUpdate := bundle.BaseCRL.NextUpdate + if !nextUpdate.IsZero() && !time.Now().After(nextUpdate) { + return bundle, nil + } + } + // ignore the cache error as it is not critical } - // try to get from cache - bundle, err = f.Cache.Get(ctx, url) + bundle, err := f.fetch(ctx, url) if err != nil { - // ignore cache error as it is not critical - return f.download(ctx, url) - + return nil, fmt.Errorf("failed to download CRL: %w", err) } - // check expiry - nextUpdate := bundle.BaseCRL.NextUpdate - if !nextUpdate.IsZero() && time.Now().After(nextUpdate) { - return f.download(ctx, url) + if f.Cache != nil { + // ignore the cache error as it is not critical + _ = f.Cache.Set(ctx, url, bundle) } return bundle, nil } -// download downloads the CRL from the given URL and saves it to the -// cache -func (f *HTTPFetcher) download(ctx context.Context, url string) (bundle *Bundle, err error) { - base, err := download(ctx, url, f.httpClient) +// fetch downloads the CRL from the given URL and saves it to the cache +func (f *HTTPFetcher) fetch(ctx context.Context, url string) (bundle *Bundle, err error) { + // fetch base CRL + base, err := fetchCRL(ctx, url, f.httpClient) if err != nil { return nil, err } - // check deltaCRL + + // check delta CRL + // TODO: support delta CRL https://github.com/notaryproject/notation-core-go/issues/228 for _, ext := range base.Extensions { if ext.Id.Equal(oidFreshestCRL) { - // TODO: support delta CRL return nil, errors.New("delta CRL is not supported") } } - bundle = &Bundle{ + return &Bundle{ BaseCRL: base, - } - - if f.Cache == nil { - // no cache, return directly - return bundle, nil - } - - // ignore the cache error as it is not critical - _ = f.Cache.Set(ctx, url, bundle) - - return bundle, nil + }, nil } -func download(ctx context.Context, baseURL string, client *http.Client) (*x509.RevocationList, error) { +func fetchCRL(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) { // validate URL - parsedURL, err := url.Parse(baseURL) + parsedURL, err := url.Parse(crlURL) if err != nil { return nil, fmt.Errorf("invalid CRL URL: %w", err) } @@ -138,7 +132,7 @@ func download(ctx context.Context, baseURL string, client *http.Client) (*x509.R } // download CRL - req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil) if err != nil { return nil, fmt.Errorf("failed to create CRL request: %w", err) } diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 6fa95600..dc1750e7 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -23,6 +23,7 @@ import ( "io" "math/big" "net/http" + "strings" "sync" "testing" "time" @@ -43,7 +44,8 @@ func TestFetch(t *testing.T) { // prepare crl certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true) crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ - Number: big.NewInt(1), + Number: big.NewInt(1), + NextUpdate: time.Now().Add(1 * time.Hour), }, certChain[1].Cert, certChain[1].PrivateKey) if err != nil { t.Fatalf("failed to create base CRL: %v", err) @@ -217,7 +219,7 @@ func TestFetch(t *testing.T) { } f.Cache = c _, err = f.Fetch(context.Background(), uncachedURL) - if err.Error() != "delta CRL is not supported" { + if !strings.Contains(err.Error(), "delta CRL is not supported") { t.Errorf("Fetcher.Fetch() error = %v, want delta CRL is not supported", err) } }) @@ -244,13 +246,13 @@ func TestFetch(t *testing.T) { func TestDownload(t *testing.T) { t.Run("parse url error", func(t *testing.T) { - _, err := download(context.Background(), ":", http.DefaultClient) + _, err := fetchCRL(context.Background(), ":", http.DefaultClient) if err == nil { t.Fatal("expected error") } }) t.Run("https download", func(t *testing.T) { - _, err := download(context.Background(), "https://example.com", http.DefaultClient) + _, err := fetchCRL(context.Background(), "https://example.com", http.DefaultClient) if err == nil { t.Fatal("expected error") } @@ -258,14 +260,14 @@ func TestDownload(t *testing.T) { t.Run("http.NewRequestWithContext error", func(t *testing.T) { var ctx context.Context = nil - _, err := download(ctx, "http://example.com", &http.Client{}) + _, err := fetchCRL(ctx, "http://example.com", &http.Client{}) if err == nil { t.Fatal("expected error") } }) t.Run("client.Do error", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ + _, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ Transport: errorRoundTripperMock{}, }) @@ -275,7 +277,7 @@ func TestDownload(t *testing.T) { }) t.Run("status code is not 2xx", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ + _, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ Transport: serverErrorRoundTripperMock{}, }) if err == nil { @@ -284,7 +286,7 @@ func TestDownload(t *testing.T) { }) t.Run("readAll error", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ + _, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ Transport: readFailedRoundTripperMock{}, }) if err == nil { @@ -293,7 +295,7 @@ func TestDownload(t *testing.T) { }) t.Run("exceed the size limit", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ + _, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, }) if err == nil { @@ -302,7 +304,7 @@ func TestDownload(t *testing.T) { }) t.Run("invalid crl", func(t *testing.T) { - _, err := download(context.Background(), "http://example.com", &http.Client{ + _, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ Transport: expectedRoundTripperMock{Body: []byte("invalid crl")}, }) if err == nil { diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index 3ab0abd8..da8de440 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -161,6 +161,7 @@ func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { // check validity now := time.Now() + // TODO IsZero return error if !crl.NextUpdate.IsZero() && now.After(crl.NextUpdate) { return fmt.Errorf("expired CRL. Current time %v is after CRL NextUpdate %v", now, crl.NextUpdate) } diff --git a/revocation/revocation.go b/revocation/revocation.go index 9c78699b..dd260768 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -70,8 +70,8 @@ type Validator interface { // revocation is an internal struct used for revocation checking type revocation struct { ocspHTTPClient *http.Client - certChainPurpose purpose.Purpose crlFetcher crlutil.Fetcher + certChainPurpose purpose.Purpose } // New constructs a revocation object for code signing certificate chain. @@ -89,8 +89,8 @@ func New(httpClient *http.Client) (Revocation, error) { return &revocation{ ocspHTTPClient: httpClient, - certChainPurpose: purpose.CodeSigning, crlFetcher: fetcher, + certChainPurpose: purpose.CodeSigning, }, nil } @@ -140,8 +140,8 @@ func NewWithOptions(opts Options) (Validator, error) { return &revocation{ ocspHTTPClient: opts.OCSPHTTPClient, - certChainPurpose: opts.CertChainPurpose, crlFetcher: fetcher, + certChainPurpose: opts.CertChainPurpose, }, nil } From dae3a2a6f0909b2ac1184497801b8525fa29a69f Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 09:40:17 +0000 Subject: [PATCH 101/110] fix: resolve comments Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 4 ++-- revocation/internal/crl/crl.go | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index e65117a1..a910dcff 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -89,7 +89,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { bundle, err := f.fetch(ctx, url) if err != nil { - return nil, fmt.Errorf("failed to download CRL: %w", err) + return nil, fmt.Errorf("failed to retrieve CRL: %w", err) } if f.Cache != nil { @@ -101,7 +101,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { } // fetch downloads the CRL from the given URL and saves it to the cache -func (f *HTTPFetcher) fetch(ctx context.Context, url string) (bundle *Bundle, err error) { +func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) { // fetch base CRL base, err := fetchCRL(ctx, url, f.httpClient) if err != nil { diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index da8de440..f2df2c32 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -160,9 +160,11 @@ func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { } // check validity + if crl.NextUpdate.IsZero() { + return errors.New("CRL NextUpdate is not set") + } now := time.Now() - // TODO IsZero return error - if !crl.NextUpdate.IsZero() && now.After(crl.NextUpdate) { + if now.After(crl.NextUpdate) { return fmt.Errorf("expired CRL. Current time %v is after CRL NextUpdate %v", now, crl.NextUpdate) } From 73c295816799bc3ed03cfb6ded7a0d8efd4a5781 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 09:53:46 +0000 Subject: [PATCH 102/110] fix: update Signed-off-by: Junjie Gao --- revocation/revocation.go | 27 ++++++------- revocation/revocation_test.go | 73 +++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/revocation/revocation.go b/revocation/revocation.go index dd260768..f249d915 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -101,14 +101,10 @@ type Options struct { // OPTIONAL. OCSPHTTPClient *http.Client - // CRLHTTPClient is the HTTP client for CRL request. If not provided, - // a default *http.Client with timeout of 5 seconds will be used. - // OPTIONAL. - CRLHTTPClient *http.Client - - // CRLCache is the cache client used to store the CRL. if not provided, - // no cache will be used. - CRLCache crlutil.Cache + // CRLFetcher is a fetcher for CRL with cache. If not provided, a default + // fetcher with an HTTP client and a timeout of 5 seconds will be used + // without cache. + CRLFetcher crlutil.Fetcher // CertChainPurpose is the purpose of the certificate chain. Supported // values are CodeSigning and Timestamping. Default value is CodeSigning. @@ -122,8 +118,13 @@ func NewWithOptions(opts Options) (Validator, error) { opts.OCSPHTTPClient = &http.Client{Timeout: 2 * time.Second} } - if opts.CRLHTTPClient == nil { - opts.CRLHTTPClient = &http.Client{Timeout: 5 * time.Second} + fetcher := opts.CRLFetcher + if fetcher == nil { + newFetcher, err := crlutil.NewHTTPFetcher(&http.Client{Timeout: 5 * time.Second}) + if err != nil { + return nil, err + } + fetcher = newFetcher } switch opts.CertChainPurpose { @@ -132,12 +133,6 @@ func NewWithOptions(opts Options) (Validator, error) { return nil, fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose) } - fetcher, err := crlutil.NewHTTPFetcher(opts.CRLHTTPClient) - if err != nil { - return nil, err - } - fetcher.Cache = opts.CRLCache - return &revocation{ ocspHTTPClient: opts.OCSPHTTPClient, crlFetcher: fetcher, diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index 2ac8b4c9..00e9597d 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -28,6 +28,7 @@ import ( "testing" "time" + "github.com/notaryproject/notation-core-go/revocation/crl" revocationocsp "github.com/notaryproject/notation-core-go/revocation/internal/ocsp" "github.com/notaryproject/notation-core-go/revocation/purpose" "github.com/notaryproject/notation-core-go/revocation/result" @@ -1035,15 +1036,20 @@ func TestCRL(t *testing.T) { t.Run("CRL check valid", func(t *testing.T) { chain := testhelper.GetRevokableRSAChainWithRevocations(3, false, true) - revocationClient, err := NewWithOptions(Options{ - CRLHTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: &crlRoundTripper{ - CertChain: chain, - Revoked: false, - }, + fetcher, err := crl.NewHTTPFetcher(&http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: false, }, + }) + if err != nil { + t.Errorf("Expected successful creation of fetcher, but received error: %v", err) + } + + revocationClient, err := NewWithOptions(Options{ OCSPHTTPClient: &http.Client{}, + CRLFetcher: fetcher, CertChainPurpose: purpose.CodeSigning, }) if err != nil { @@ -1084,15 +1090,20 @@ func TestCRL(t *testing.T) { t.Run("CRL check with revoked status", func(t *testing.T) { chain := testhelper.GetRevokableRSAChainWithRevocations(3, false, true) - revocationClient, err := NewWithOptions(Options{ - CRLHTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: &crlRoundTripper{ - CertChain: chain, - Revoked: true, - }, + fetcher, err := crl.NewHTTPFetcher(&http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: true, }, + }) + if err != nil { + t.Errorf("Expected successful creation of fetcher, but received error: %v", err) + } + + revocationClient, err := NewWithOptions(Options{ OCSPHTTPClient: &http.Client{}, + CRLFetcher: fetcher, CertChainPurpose: purpose.CodeSigning, }) if err != nil { @@ -1140,17 +1151,21 @@ func TestCRL(t *testing.T) { t.Run("OCSP fallback to CRL", func(t *testing.T) { chain := testhelper.GetRevokableRSAChainWithRevocations(3, true, true) + fetcher, err := crl.NewHTTPFetcher(&http.Client{ + Timeout: 5 * time.Second, + Transport: &crlRoundTripper{ + CertChain: chain, + Revoked: true, + FailOCSP: true, + }, + }) + if err != nil { + t.Errorf("Expected successful creation of fetcher, but received error: %v", err) + } revocationClient, err := NewWithOptions(Options{ - CRLHTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: &crlRoundTripper{ - CertChain: chain, - Revoked: true, - FailOCSP: true, - }, - }, OCSPHTTPClient: &http.Client{}, + CRLFetcher: fetcher, CertChainPurpose: purpose.CodeSigning, }) if err != nil { @@ -1218,9 +1233,14 @@ func TestPanicHandling(t *testing.T) { Transport: panicTransport{}, } + fetcher, err := crl.NewHTTPFetcher(client) + if err != nil { + t.Errorf("Expected successful creation of fetcher, but received error: %v", err) + } + r, err := NewWithOptions(Options{ OCSPHTTPClient: client, - CRLHTTPClient: client, + CRLFetcher: fetcher, CertChainPurpose: purpose.CodeSigning, }) if err != nil { @@ -1245,9 +1265,14 @@ func TestPanicHandling(t *testing.T) { Transport: panicTransport{}, } + fetcher, err := crl.NewHTTPFetcher(client) + if err != nil { + t.Errorf("Expected successful creation of fetcher, but received error: %v", err) + } + r, err := NewWithOptions(Options{ OCSPHTTPClient: client, - CRLHTTPClient: client, + CRLFetcher: fetcher, CertChainPurpose: purpose.CodeSigning, }) if err != nil { From 7ed8899a94e9a74c77aab09fd0f65f4ffd35b51c Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Thu, 19 Sep 2024 12:29:28 +0000 Subject: [PATCH 103/110] fix: resolve comments Signed-off-by: Junjie Gao --- revocation/crl/errors.go | 2 +- revocation/crl/fetcher.go | 6 +++--- revocation/internal/crl/crl.go | 2 +- revocation/internal/crl/crl_test.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/revocation/crl/errors.go b/revocation/crl/errors.go index 2fdae9f5..a1978910 100644 --- a/revocation/crl/errors.go +++ b/revocation/crl/errors.go @@ -15,5 +15,5 @@ package crl import "errors" -// ErrCacheMiss is an error type for when a cache miss occurs +// ErrCacheMiss is returned when a cache miss occurs. var ErrCacheMiss = errors.New("cache miss") diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index a910dcff..ff583f3c 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package crl provides Fetcher and Cache interface and implementations for -// fetching CRLs. +// Package crl provides Fetcher interface with its implementation, and the +// Cache interface. package crl import ( @@ -100,7 +100,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { return bundle, nil } -// fetch downloads the CRL from the given URL and saves it to the cache +// fetch downloads the CRL from the given URL. func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) { // fetch base CRL base, err := fetchCRL(ctx, url, f.httpClient) diff --git a/revocation/internal/crl/crl.go b/revocation/internal/crl/crl.go index f2df2c32..50f2c085 100644 --- a/revocation/internal/crl/crl.go +++ b/revocation/internal/crl/crl.go @@ -78,7 +78,7 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C Result: result.ResultUnknown, ServerResults: []*result.ServerResult{{ RevocationMethod: result.RevocationMethodCRL, - Error: errors.New("CRL fetcher is nil"), + Error: errors.New("CRL fetcher cannot be nil"), Result: result.ResultUnknown, }}, RevocationMethod: result.RevocationMethodCRL, diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 55fb4010..6f4f47e2 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -48,8 +48,8 @@ func TestCertCheckStatus(t *testing.T) { CRLDistributionPoints: []string{"http://example.com"}, } r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{}) - if r.ServerResults[0].Error.Error() != "CRL fetcher is nil" { - t.Fatalf("expected CRL fetcher is nil, got %v", r.ServerResults[0].Error) + if r.ServerResults[0].Error.Error() != "CRL fetcher cannot be nil" { + t.Fatalf("expected CRL fetcher cannot be nil, got %v", r.ServerResults[0].Error) } }) From 00e8bce3e9638c2b86d5dfc0c7e47b6bcd87f0a4 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 20 Sep 2024 03:58:18 +0000 Subject: [PATCH 104/110] fix: added DiscardCacheError in fetcher Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 13 ++++++++++--- revocation/crl/fetcher_test.go | 4 ++++ revocation/internal/crl/crl_test.go | 7 +++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index ff583f3c..f0dac064 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -51,6 +51,9 @@ type HTTPFetcher struct { // If Cache is nil, no cache is used. Cache Cache + // DiscardCacheError specifies whether to discard the cache on error. + DiscardCacheError bool + httpClient *http.Client } @@ -84,7 +87,9 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { return bundle, nil } } - // ignore the cache error as it is not critical + if err != nil && !f.DiscardCacheError { + return nil, fmt.Errorf("failed to retrieve CRL from cache: %w", err) + } } bundle, err := f.fetch(ctx, url) @@ -93,8 +98,10 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { } if f.Cache != nil { - // ignore the cache error as it is not critical - _ = f.Cache.Set(ctx, url, bundle) + err = f.Cache.Set(ctx, url, bundle) + if err != nil && !f.DiscardCacheError { + return nil, fmt.Errorf("failed to store CRL to cache: %w", err) + } } return bundle, nil diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index dc1750e7..47c8370a 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -140,6 +140,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c + f.DiscardCacheError = true bundle, err := f.Fetch(context.Background(), uncachedURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) @@ -182,6 +183,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c + f.DiscardCacheError = true bundle, err = f.Fetch(context.Background(), expiredCRLURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) @@ -218,6 +220,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c + f.DiscardCacheError = true _, err = f.Fetch(context.Background(), uncachedURL) if !strings.Contains(err.Error(), "delta CRL is not supported") { t.Errorf("Fetcher.Fetch() error = %v, want delta CRL is not supported", err) @@ -234,6 +237,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c + f.DiscardCacheError = true bundle, err = f.Fetch(context.Background(), exampleURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 6f4f47e2..5555374f 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -127,6 +127,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -166,6 +167,7 @@ func TestCertCheckStatus(t *testing.T) { } fetcher.Cache = memoryCache + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -192,6 +194,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -223,6 +226,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -264,6 +268,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -285,6 +290,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -314,6 +320,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) From 335d7d997f25c3372ab0f892b57da14f384b6bbb Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 20 Sep 2024 04:06:09 +0000 Subject: [PATCH 105/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index f0dac064..520d837d 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -51,7 +51,7 @@ type HTTPFetcher struct { // If Cache is nil, no cache is used. Cache Cache - // DiscardCacheError specifies whether to discard the cache on error. + // DiscardCacheError specifies whether to discard any error on cache. DiscardCacheError bool httpClient *http.Client @@ -86,8 +86,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { if !nextUpdate.IsZero() && !time.Now().After(nextUpdate) { return bundle, nil } - } - if err != nil && !f.DiscardCacheError { + } else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheError { return nil, fmt.Errorf("failed to retrieve CRL from cache: %w", err) } } From cd5bbc01428281d1bf57e69299128b022c92bc40 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 20 Sep 2024 04:09:03 +0000 Subject: [PATCH 106/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 8 ++++---- revocation/crl/fetcher_test.go | 8 ++++---- revocation/internal/crl/crl_test.go | 14 +++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 520d837d..f284ab57 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -51,8 +51,8 @@ type HTTPFetcher struct { // If Cache is nil, no cache is used. Cache Cache - // DiscardCacheError specifies whether to discard any error on cache. - DiscardCacheError bool + // DiscardCacheFailure specifies whether to discard any error on cache. + DiscardCacheFailure bool httpClient *http.Client } @@ -86,7 +86,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { if !nextUpdate.IsZero() && !time.Now().After(nextUpdate) { return bundle, nil } - } else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheError { + } else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheFailure { return nil, fmt.Errorf("failed to retrieve CRL from cache: %w", err) } } @@ -98,7 +98,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { if f.Cache != nil { err = f.Cache.Set(ctx, url, bundle) - if err != nil && !f.DiscardCacheError { + if err != nil && !f.DiscardCacheFailure { return nil, fmt.Errorf("failed to store CRL to cache: %w", err) } } diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index 47c8370a..b7b01255 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -140,7 +140,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheError = true + f.DiscardCacheFailure = false bundle, err := f.Fetch(context.Background(), uncachedURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) @@ -183,7 +183,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheError = true + f.DiscardCacheFailure = true bundle, err = f.Fetch(context.Background(), expiredCRLURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) @@ -220,7 +220,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheError = true + f.DiscardCacheFailure = true _, err = f.Fetch(context.Background(), uncachedURL) if !strings.Contains(err.Error(), "delta CRL is not supported") { t.Errorf("Fetcher.Fetch() error = %v, want delta CRL is not supported", err) @@ -237,7 +237,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheError = true + f.DiscardCacheFailure = true bundle, err = f.Fetch(context.Background(), exampleURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 5555374f..732f4cde 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -127,7 +127,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheError = true + fetcher.DiscardCacheFailure = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -167,7 +167,7 @@ func TestCertCheckStatus(t *testing.T) { } fetcher.Cache = memoryCache - fetcher.DiscardCacheError = true + fetcher.DiscardCacheFailure = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -194,7 +194,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheError = true + fetcher.DiscardCacheFailure = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -226,7 +226,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheError = true + fetcher.DiscardCacheFailure = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -268,7 +268,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheError = true + fetcher.DiscardCacheFailure = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -290,7 +290,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheError = true + fetcher.DiscardCacheFailure = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -320,7 +320,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheError = true + fetcher.DiscardCacheFailure = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) From f21a22e6d922a98bfe8974681e5d5a5844fae29f Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 20 Sep 2024 04:18:43 +0000 Subject: [PATCH 107/110] test: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher_test.go | 54 +++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index b7b01255..f951d6de 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -19,6 +19,7 @@ import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" + "errors" "fmt" "io" "math/big" @@ -228,7 +229,10 @@ func TestFetch(t *testing.T) { }) t.Run("Set cache error", func(t *testing.T) { - c := &errorCache{} + c := &errorCache{ + GetError: ErrCacheMiss, + SetError: errors.New("cache error"), + } httpClient := &http.Client{ Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, } @@ -246,6 +250,45 @@ func TestFetch(t *testing.T) { t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw) } }) + + t.Run("Get error without discard", func(t *testing.T) { + c := &errorCache{ + GetError: errors.New("cache error"), + } + httpClient := &http.Client{ + Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, + } + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } + f.Cache = c + f.DiscardCacheFailure = false + _, err = f.Fetch(context.Background(), exampleURL) + if !strings.HasPrefix(err.Error(), "failed to retrieve CRL from cache:") { + t.Errorf("Fetcher.Fetch() error = %v, want failed to retrieve CRL from cache:", err) + } + }) + + t.Run("Set error without discard", func(t *testing.T) { + c := &errorCache{ + GetError: ErrCacheMiss, + SetError: errors.New("cache error"), + } + httpClient := &http.Client{ + Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, + } + f, err := NewHTTPFetcher(httpClient) + if err != nil { + t.Errorf("NewHTTPFetcher() error = %v, want nil", err) + } + f.Cache = c + f.DiscardCacheFailure = false + _, err = f.Fetch(context.Background(), exampleURL) + if !strings.HasPrefix(err.Error(), "failed to store CRL to cache:") { + t.Errorf("Fetcher.Fetch() error = %v, want failed to store CRL to cache:", err) + } + }) } func TestDownload(t *testing.T) { @@ -391,12 +434,15 @@ func (c *memoryCache) Set(ctx context.Context, url string, bundle *Bundle) error return nil } -type errorCache struct{} +type errorCache struct { + GetError error + SetError error +} func (c *errorCache) Get(ctx context.Context, url string) (*Bundle, error) { - return nil, fmt.Errorf("Get error") + return nil, c.GetError } func (c *errorCache) Set(ctx context.Context, url string, bundle *Bundle) error { - return fmt.Errorf("Set error") + return c.SetError } From 375867e706637e96ef3671ef11d74e366dff7d9e Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 20 Sep 2024 04:19:50 +0000 Subject: [PATCH 108/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index f284ab57..2ae63807 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -52,6 +52,8 @@ type HTTPFetcher struct { Cache Cache // DiscardCacheFailure specifies whether to discard any error on cache. + // + // ErrCacheMiss is not considered as an error if DiscardCacheFailure is true DiscardCacheFailure bool httpClient *http.Client From 67410022e99952ccc02716e51189a29285554e56 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 20 Sep 2024 04:20:22 +0000 Subject: [PATCH 109/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 2ae63807..2edad6d8 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -53,7 +53,7 @@ type HTTPFetcher struct { // DiscardCacheFailure specifies whether to discard any error on cache. // - // ErrCacheMiss is not considered as an error if DiscardCacheFailure is true + // ErrCacheMiss is not considered as an error DiscardCacheFailure bool httpClient *http.Client From 4b3bd0efa57362cb6d7d42f3f0562600945d2167 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 20 Sep 2024 04:22:32 +0000 Subject: [PATCH 110/110] fix: update Signed-off-by: Junjie Gao --- revocation/crl/fetcher.go | 11 ++++++----- revocation/crl/fetcher_test.go | 12 ++++++------ revocation/internal/crl/crl_test.go | 14 +++++++------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/revocation/crl/fetcher.go b/revocation/crl/fetcher.go index 2edad6d8..cef1a7b5 100644 --- a/revocation/crl/fetcher.go +++ b/revocation/crl/fetcher.go @@ -51,10 +51,11 @@ type HTTPFetcher struct { // If Cache is nil, no cache is used. Cache Cache - // DiscardCacheFailure specifies whether to discard any error on cache. + // DiscardCacheError specifies whether to discard any error on cache. // - // ErrCacheMiss is not considered as an error - DiscardCacheFailure bool + // ErrCacheMiss is not considered as an failure and will not be returned as + // an error if DiscardCacheError is false. + DiscardCacheError bool httpClient *http.Client } @@ -88,7 +89,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { if !nextUpdate.IsZero() && !time.Now().After(nextUpdate) { return bundle, nil } - } else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheFailure { + } else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheError { return nil, fmt.Errorf("failed to retrieve CRL from cache: %w", err) } } @@ -100,7 +101,7 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) { if f.Cache != nil { err = f.Cache.Set(ctx, url, bundle) - if err != nil && !f.DiscardCacheFailure { + if err != nil && !f.DiscardCacheError { return nil, fmt.Errorf("failed to store CRL to cache: %w", err) } } diff --git a/revocation/crl/fetcher_test.go b/revocation/crl/fetcher_test.go index f951d6de..9b22f97e 100644 --- a/revocation/crl/fetcher_test.go +++ b/revocation/crl/fetcher_test.go @@ -141,7 +141,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheFailure = false + f.DiscardCacheError = false bundle, err := f.Fetch(context.Background(), uncachedURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) @@ -184,7 +184,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheFailure = true + f.DiscardCacheError = true bundle, err = f.Fetch(context.Background(), expiredCRLURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) @@ -221,7 +221,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheFailure = true + f.DiscardCacheError = true _, err = f.Fetch(context.Background(), uncachedURL) if !strings.Contains(err.Error(), "delta CRL is not supported") { t.Errorf("Fetcher.Fetch() error = %v, want delta CRL is not supported", err) @@ -241,7 +241,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheFailure = true + f.DiscardCacheError = true bundle, err = f.Fetch(context.Background(), exampleURL) if err != nil { t.Errorf("Fetcher.Fetch() error = %v, want nil", err) @@ -263,7 +263,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheFailure = false + f.DiscardCacheError = false _, err = f.Fetch(context.Background(), exampleURL) if !strings.HasPrefix(err.Error(), "failed to retrieve CRL from cache:") { t.Errorf("Fetcher.Fetch() error = %v, want failed to retrieve CRL from cache:", err) @@ -283,7 +283,7 @@ func TestFetch(t *testing.T) { t.Errorf("NewHTTPFetcher() error = %v, want nil", err) } f.Cache = c - f.DiscardCacheFailure = false + f.DiscardCacheError = false _, err = f.Fetch(context.Background(), exampleURL) if !strings.HasPrefix(err.Error(), "failed to store CRL to cache:") { t.Errorf("Fetcher.Fetch() error = %v, want failed to store CRL to cache:", err) diff --git a/revocation/internal/crl/crl_test.go b/revocation/internal/crl/crl_test.go index 732f4cde..5555374f 100644 --- a/revocation/internal/crl/crl_test.go +++ b/revocation/internal/crl/crl_test.go @@ -127,7 +127,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheFailure = true + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -167,7 +167,7 @@ func TestCertCheckStatus(t *testing.T) { } fetcher.Cache = memoryCache - fetcher.DiscardCacheFailure = true + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -194,7 +194,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheFailure = true + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -226,7 +226,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheFailure = true + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -268,7 +268,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheFailure = true + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -290,7 +290,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheFailure = true + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, }) @@ -320,7 +320,7 @@ func TestCertCheckStatus(t *testing.T) { t.Fatal(err) } fetcher.Cache = memoryCache - fetcher.DiscardCacheFailure = true + fetcher.DiscardCacheError = true r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{ Fetcher: fetcher, })