|
| 1 | +package cose |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "io" |
| 7 | + "maps" |
| 8 | +) |
| 9 | + |
| 10 | +// HashEnvelopePayload indicates the payload of a Hash_Envelope object. |
| 11 | +// It is used by the [SignHashEnvelope] function. |
| 12 | +// |
| 13 | +// # Experimental |
| 14 | +// |
| 15 | +// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or |
| 16 | +// removed in a later release. |
| 17 | +type HashEnvelopePayload struct { |
| 18 | + // HashAlgorithm is the hash algorithm used to produce the hash value. |
| 19 | + HashAlgorithm Algorithm |
| 20 | + |
| 21 | + // HashValue is the hash value of the payload. |
| 22 | + HashValue []byte |
| 23 | + |
| 24 | + // PreimageContentType is the content type of the data that has been hashed. |
| 25 | + // The value is either an unsigned integer (RFC 7252 Section 12.3) or a |
| 26 | + // string (RFC 9110 Section 8.3). |
| 27 | + // This field is optional. |
| 28 | + // |
| 29 | + // References: |
| 30 | + // - https://www.iana.org/assignments/core-parameters/core-parameters.xhtml |
| 31 | + // - https://www.iana.org/assignments/media-types/media-types.xhtml |
| 32 | + PreimageContentType any // uint / string |
| 33 | + |
| 34 | + // Location is the location of the hash value in the payload. |
| 35 | + // This field is optional. |
| 36 | + Location string |
| 37 | +} |
| 38 | + |
| 39 | +// SignHashEnvelope signs a [Sign1Message] using the provided [Signer] and |
| 40 | +// produces a Hash_Envelope object. |
| 41 | +// |
| 42 | +// Hash_Envelope_Protected_Header = { |
| 43 | +// ? &(alg: 1) => int, |
| 44 | +// &(payload_hash_alg: 258) => int |
| 45 | +// &(payload_preimage_content_type: 259) => uint / tstr |
| 46 | +// ? &(payload_location: 260) => tstr |
| 47 | +// * int / tstr => any |
| 48 | +// } |
| 49 | +// |
| 50 | +// Hash_Envelope_Unprotected_Header = { |
| 51 | +// * int / tstr => any |
| 52 | +// } |
| 53 | +// |
| 54 | +// Hash_Envelope_as_COSE_Sign1 = [ |
| 55 | +// protected : bstr .cbor Hash_Envelope_Protected_Header, |
| 56 | +// unprotected : Hash_Envelope_Unprotected_Header, |
| 57 | +// payload: bstr / nil, |
| 58 | +// signature : bstr |
| 59 | +// ] |
| 60 | +// |
| 61 | +// Hash_Envelope = #6.18(Hash_Envelope_as_COSE_Sign1) |
| 62 | +// |
| 63 | +// Reference: https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html |
| 64 | +// |
| 65 | +// # Experimental |
| 66 | +// |
| 67 | +// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or |
| 68 | +// removed in a later release. |
| 69 | +func SignHashEnvelope(rand io.Reader, signer Signer, headers Headers, payload HashEnvelopePayload) ([]byte, error) { |
| 70 | + if err := validateHash(payload.HashAlgorithm, payload.HashValue); err != nil { |
| 71 | + return nil, err |
| 72 | + } |
| 73 | + |
| 74 | + headers.Protected = setHashEnvelopeProtectedHeader(headers.Protected, &payload) |
| 75 | + headers.RawProtected = nil |
| 76 | + if err := validateHashEnvelopeHeaders(&headers); err != nil { |
| 77 | + return nil, err |
| 78 | + } |
| 79 | + |
| 80 | + return Sign1(rand, signer, headers, payload.HashValue, nil) |
| 81 | +} |
| 82 | + |
| 83 | +// VerifyHashEnvelope verifies a Hash_Envelope object using the provided |
| 84 | +// [Verifier]. |
| 85 | +// It returns the decoded [Sign1Message] if the verification is successful. |
| 86 | +// |
| 87 | +// # Experimental |
| 88 | +// |
| 89 | +// Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or |
| 90 | +// removed in a later release. |
| 91 | +func VerifyHashEnvelope(verifier Verifier, envelope []byte) (*Sign1Message, error) { |
| 92 | + // parse and validate the Hash_Envelope object |
| 93 | + var message Sign1Message |
| 94 | + if err := message.UnmarshalCBOR(envelope); err != nil { |
| 95 | + return nil, err |
| 96 | + } |
| 97 | + if err := validateHashEnvelopeHeaders(&message.Headers); err != nil { |
| 98 | + return nil, err |
| 99 | + } |
| 100 | + |
| 101 | + // verify the Hash_Envelope object |
| 102 | + if err := message.Verify(nil, verifier); err != nil { |
| 103 | + return nil, err |
| 104 | + } |
| 105 | + |
| 106 | + // cast to type Algorithm |
| 107 | + hashAlgorithm, err := message.Headers.Protected.PayloadHashAlgorithm() |
| 108 | + if err != nil { |
| 109 | + return nil, err |
| 110 | + } |
| 111 | + message.Headers.Protected[HeaderLabelPayloadHashAlgorithm] = hashAlgorithm |
| 112 | + |
| 113 | + // validate the hash value |
| 114 | + if err := validateHash(hashAlgorithm, message.Payload); err != nil { |
| 115 | + return nil, err |
| 116 | + } |
| 117 | + |
| 118 | + return &message, nil |
| 119 | +} |
| 120 | + |
| 121 | +// validateHash checks the validity of the known hash. |
| 122 | +func validateHash(alg Algorithm, value []byte) error { |
| 123 | + hash := alg.hashFunc() |
| 124 | + if hash == 0 { |
| 125 | + return nil // no check on unsupported hash algorithms |
| 126 | + } |
| 127 | + if size := hash.Size(); size != len(value) { |
| 128 | + return fmt.Errorf("%v: size mismatch: expected %d, got %d", alg, size, len(value)) |
| 129 | + } |
| 130 | + return nil |
| 131 | +} |
| 132 | + |
| 133 | +// setHashEnvelopeProtectedHeader sets the protected header for a Hash_Envelope |
| 134 | +// object. |
| 135 | +func setHashEnvelopeProtectedHeader(base ProtectedHeader, payload *HashEnvelopePayload) ProtectedHeader { |
| 136 | + header := maps.Clone(base) |
| 137 | + if header == nil { |
| 138 | + header = make(ProtectedHeader) |
| 139 | + } |
| 140 | + header[HeaderLabelPayloadHashAlgorithm] = payload.HashAlgorithm |
| 141 | + if payload.PreimageContentType != nil { |
| 142 | + header[HeaderLabelPayloadPreimageContentType] = payload.PreimageContentType |
| 143 | + } |
| 144 | + if payload.Location != "" { |
| 145 | + header[HeaderLabelPayloadLocation] = payload.Location |
| 146 | + } |
| 147 | + return header |
| 148 | +} |
| 149 | + |
| 150 | +// validateHashEnvelopeHeaders validates the headers of a Hash_Envelope object. |
| 151 | +// See https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html |
| 152 | +// section 4 for more details. |
| 153 | +func validateHashEnvelopeHeaders(headers *Headers) error { |
| 154 | + var foundPayloadHashAlgorithm bool |
| 155 | + for label, value := range headers.Protected { |
| 156 | + // Validate that all header labels are integers or strings. |
| 157 | + // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4 |
| 158 | + label, ok := normalizeLabel(label) |
| 159 | + if !ok { |
| 160 | + return errors.New("header label: require int / tstr type") |
| 161 | + } |
| 162 | + |
| 163 | + switch label { |
| 164 | + case HeaderLabelContentType: |
| 165 | + return errors.New("protected header parameter: content type: not allowed") |
| 166 | + case HeaderLabelPayloadHashAlgorithm: |
| 167 | + _, isAlg := value.(Algorithm) |
| 168 | + if !isAlg && !canInt(value) { |
| 169 | + return errors.New("protected header parameter: payload hash alg: require int type") |
| 170 | + } |
| 171 | + foundPayloadHashAlgorithm = true |
| 172 | + case HeaderLabelPayloadPreimageContentType: |
| 173 | + if !canUint(value) && !canTstr(value) { |
| 174 | + return errors.New("protected header parameter: payload preimage content type: require uint / tstr type") |
| 175 | + } |
| 176 | + case HeaderLabelPayloadLocation: |
| 177 | + if !canTstr(value) { |
| 178 | + return errors.New("protected header parameter: payload location: require tstr type") |
| 179 | + } |
| 180 | + } |
| 181 | + } |
| 182 | + if !foundPayloadHashAlgorithm { |
| 183 | + return errors.New("protected header parameter: payload hash alg: required") |
| 184 | + } |
| 185 | + |
| 186 | + for label := range headers.Unprotected { |
| 187 | + // Validate that all header labels are integers or strings. |
| 188 | + // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4 |
| 189 | + label, ok := normalizeLabel(label) |
| 190 | + if !ok { |
| 191 | + return errors.New("header label: require int / tstr type") |
| 192 | + } |
| 193 | + |
| 194 | + switch label { |
| 195 | + case HeaderLabelContentType: |
| 196 | + return errors.New("unprotected header parameter: content type: not allowed") |
| 197 | + case HeaderLabelPayloadHashAlgorithm: |
| 198 | + return errors.New("unprotected header parameter: payload hash alg: not allowed") |
| 199 | + case HeaderLabelPayloadPreimageContentType: |
| 200 | + return errors.New("unprotected header parameter: payload preimage content type: not allowed") |
| 201 | + case HeaderLabelPayloadLocation: |
| 202 | + return errors.New("unprotected header parameter: payload location: not allowed") |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + return nil |
| 207 | +} |
0 commit comments