| /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
| /* vim: set ts=8 sts=2 et sw=2 tw=80: */ |
| /* This code is made available to you under your choice of the following sets |
| * of licensing terms: |
| */ |
| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| */ |
| /* Copyright 2013 Mozilla Contributors |
| * |
| * 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. |
| */ |
| |
| #include "mozpkix/pkixder.h" |
| |
| #include "mozpkix/pkixutil.h" |
| |
| namespace mozilla { namespace pkix { namespace der { |
| |
| // Too complicated to be inline |
| Result |
| ReadTagAndGetValue(Reader& input, /*out*/ uint8_t& tag, /*out*/ Input& value) |
| { |
| Result rv; |
| |
| rv = input.Read(tag); |
| if (rv != Success) { |
| return rv; |
| } |
| if ((tag & 0x1F) == 0x1F) { |
| return Result::ERROR_BAD_DER; // high tag number form not allowed |
| } |
| |
| uint16_t length; |
| |
| // The short form of length is a single byte with the high order bit set |
| // to zero. The long form of length is one byte with the high order bit |
| // set, followed by N bytes, where N is encoded in the lowest 7 bits of |
| // the first byte. |
| uint8_t length1; |
| rv = input.Read(length1); |
| if (rv != Success) { |
| return rv; |
| } |
| if (!(length1 & 0x80)) { |
| length = length1; |
| } else if (length1 == 0x81) { |
| uint8_t length2; |
| rv = input.Read(length2); |
| if (rv != Success) { |
| return rv; |
| } |
| if (length2 < 128) { |
| // Not shortest possible encoding |
| return Result::ERROR_BAD_DER; |
| } |
| length = length2; |
| } else if (length1 == 0x82) { |
| rv = input.Read(length); |
| if (rv != Success) { |
| return rv; |
| } |
| if (length < 256) { |
| // Not shortest possible encoding |
| return Result::ERROR_BAD_DER; |
| } |
| } else { |
| // We don't support lengths larger than 2^16 - 1. |
| return Result::ERROR_BAD_DER; |
| } |
| |
| return input.Skip(length, value); |
| } |
| |
| static Result |
| OptionalNull(Reader& input) |
| { |
| if (input.Peek(NULLTag)) { |
| return Null(input); |
| } |
| return Success; |
| } |
| |
| namespace { |
| |
| Result |
| AlgorithmIdentifierValue(Reader& input, /*out*/ Reader& algorithmOIDValue) |
| { |
| Result rv = ExpectTagAndGetValue(input, der::OIDTag, algorithmOIDValue); |
| if (rv != Success) { |
| return rv; |
| } |
| return OptionalNull(input); |
| } |
| |
| } // namespace |
| |
| Result |
| SignatureAlgorithmIdentifierValue(Reader& input, |
| /*out*/ PublicKeyAlgorithm& publicKeyAlgorithm, |
| /*out*/ DigestAlgorithm& digestAlgorithm) |
| { |
| // RFC 5758 Section 3.2 (ECDSA with SHA-2), and RFC 3279 Section 2.2.3 |
| // (ECDSA with SHA-1) say that parameters must be omitted. |
| // |
| // RFC 4055 Section 5 and RFC 3279 Section 2.2.1 both say that parameters for |
| // RSA must be encoded as NULL; we relax that requirement by allowing the |
| // NULL to be omitted, to match all the other signature algorithms we support |
| // and for compatibility. |
| Reader algorithmID; |
| Result rv = AlgorithmIdentifierValue(input, algorithmID); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| // RFC 5758 Section 3.2 (ecdsa-with-SHA224 is intentionally excluded) |
| // python DottedOIDToCode.py ecdsa-with-SHA256 1.2.840.10045.4.3.2 |
| static const uint8_t ecdsa_with_SHA256[] = { |
| 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02 |
| }; |
| // python DottedOIDToCode.py ecdsa-with-SHA384 1.2.840.10045.4.3.3 |
| static const uint8_t ecdsa_with_SHA384[] = { |
| 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03 |
| }; |
| // python DottedOIDToCode.py ecdsa-with-SHA512 1.2.840.10045.4.3.4 |
| static const uint8_t ecdsa_with_SHA512[] = { |
| 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x04 |
| }; |
| |
| // RFC 4055 Section 5 (sha224WithRSAEncryption is intentionally excluded) |
| // python DottedOIDToCode.py sha256WithRSAEncryption 1.2.840.113549.1.1.11 |
| static const uint8_t sha256WithRSAEncryption[] = { |
| 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b |
| }; |
| // python DottedOIDToCode.py sha384WithRSAEncryption 1.2.840.113549.1.1.12 |
| static const uint8_t sha384WithRSAEncryption[] = { |
| 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0c |
| }; |
| // python DottedOIDToCode.py sha512WithRSAEncryption 1.2.840.113549.1.1.13 |
| static const uint8_t sha512WithRSAEncryption[] = { |
| 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0d |
| }; |
| |
| // RFC 3279 Section 2.2.1 |
| // python DottedOIDToCode.py sha-1WithRSAEncryption 1.2.840.113549.1.1.5 |
| static const uint8_t sha_1WithRSAEncryption[] = { |
| 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05 |
| }; |
| |
| // NIST Open Systems Environment (OSE) Implementor's Workshop (OIW) |
| // http://www.oiw.org/agreements/stable/12s-9412.txt (no longer works). |
| // http://www.imc.org/ietf-pkix/old-archive-97/msg01166.html |
| // We need to support this this non-PKIX OID for compatibility. |
| // python DottedOIDToCode.py sha1WithRSASignature 1.3.14.3.2.29 |
| static const uint8_t sha1WithRSASignature[] = { |
| 0x2b, 0x0e, 0x03, 0x02, 0x1d |
| }; |
| |
| // RFC 3279 Section 2.2.3 |
| // python DottedOIDToCode.py ecdsa-with-SHA1 1.2.840.10045.4.1 |
| static const uint8_t ecdsa_with_SHA1[] = { |
| 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x01 |
| }; |
| |
| // Matching is attempted based on a rough estimate of the commonality of the |
| // algorithm, to minimize the number of MatchRest calls. |
| if (algorithmID.MatchRest(sha256WithRSAEncryption)) { |
| publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; |
| digestAlgorithm = DigestAlgorithm::sha256; |
| } else if (algorithmID.MatchRest(ecdsa_with_SHA256)) { |
| publicKeyAlgorithm = PublicKeyAlgorithm::ECDSA; |
| digestAlgorithm = DigestAlgorithm::sha256; |
| } else if (algorithmID.MatchRest(sha_1WithRSAEncryption)) { |
| publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; |
| digestAlgorithm = DigestAlgorithm::sha1; |
| } else if (algorithmID.MatchRest(ecdsa_with_SHA1)) { |
| publicKeyAlgorithm = PublicKeyAlgorithm::ECDSA; |
| digestAlgorithm = DigestAlgorithm::sha1; |
| } else if (algorithmID.MatchRest(ecdsa_with_SHA384)) { |
| publicKeyAlgorithm = PublicKeyAlgorithm::ECDSA; |
| digestAlgorithm = DigestAlgorithm::sha384; |
| } else if (algorithmID.MatchRest(ecdsa_with_SHA512)) { |
| publicKeyAlgorithm = PublicKeyAlgorithm::ECDSA; |
| digestAlgorithm = DigestAlgorithm::sha512; |
| } else if (algorithmID.MatchRest(sha384WithRSAEncryption)) { |
| publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; |
| digestAlgorithm = DigestAlgorithm::sha384; |
| } else if (algorithmID.MatchRest(sha512WithRSAEncryption)) { |
| publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; |
| digestAlgorithm = DigestAlgorithm::sha512; |
| } else if (algorithmID.MatchRest(sha1WithRSASignature)) { |
| // XXX(bug 1042479): recognize this old OID for compatibility. |
| publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; |
| digestAlgorithm = DigestAlgorithm::sha1; |
| } else { |
| return Result::ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED; |
| } |
| |
| return Success; |
| } |
| |
| Result |
| DigestAlgorithmIdentifier(Reader& input, /*out*/ DigestAlgorithm& algorithm) |
| { |
| return der::Nested(input, SEQUENCE, [&algorithm](Reader& r) -> Result { |
| Reader algorithmID; |
| Result rv = AlgorithmIdentifierValue(r, algorithmID); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| // RFC 4055 Section 2.1 |
| // python DottedOIDToCode.py id-sha1 1.3.14.3.2.26 |
| static const uint8_t id_sha1[] = { |
| 0x2b, 0x0e, 0x03, 0x02, 0x1a |
| }; |
| // python DottedOIDToCode.py id-sha256 2.16.840.1.101.3.4.2.1 |
| static const uint8_t id_sha256[] = { |
| 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01 |
| }; |
| // python DottedOIDToCode.py id-sha384 2.16.840.1.101.3.4.2.2 |
| static const uint8_t id_sha384[] = { |
| 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02 |
| }; |
| // python DottedOIDToCode.py id-sha512 2.16.840.1.101.3.4.2.3 |
| static const uint8_t id_sha512[] = { |
| 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03 |
| }; |
| |
| // Matching is attempted based on a rough estimate of the commonality of the |
| // algorithm, to minimize the number of MatchRest calls. |
| if (algorithmID.MatchRest(id_sha1)) { |
| algorithm = DigestAlgorithm::sha1; |
| } else if (algorithmID.MatchRest(id_sha256)) { |
| algorithm = DigestAlgorithm::sha256; |
| } else if (algorithmID.MatchRest(id_sha384)) { |
| algorithm = DigestAlgorithm::sha384; |
| } else if (algorithmID.MatchRest(id_sha512)) { |
| algorithm = DigestAlgorithm::sha512; |
| } else { |
| return Result::ERROR_INVALID_ALGORITHM; |
| } |
| |
| return Success; |
| }); |
| } |
| |
| Result |
| SignedData(Reader& input, /*out*/ Reader& tbs, |
| /*out*/ SignedDataWithSignature& signedData) |
| { |
| Reader::Mark mark(input.GetMark()); |
| |
| Result rv; |
| rv = ExpectTagAndGetValue(input, SEQUENCE, tbs); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| rv = input.GetInput(mark, signedData.data); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| rv = ExpectTagAndGetValue(input, der::SEQUENCE, signedData.algorithm); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| rv = BitStringWithNoUnusedBits(input, signedData.signature); |
| if (rv == Result::ERROR_BAD_DER) { |
| rv = Result::ERROR_BAD_SIGNATURE; |
| } |
| return rv; |
| } |
| |
| Result |
| BitStringWithNoUnusedBits(Reader& input, /*out*/ Input& value) |
| { |
| Reader valueWithUnusedBits; |
| Result rv = ExpectTagAndGetValue(input, BIT_STRING, valueWithUnusedBits); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| uint8_t unusedBitsAtEnd; |
| if (valueWithUnusedBits.Read(unusedBitsAtEnd) != Success) { |
| return Result::ERROR_BAD_DER; |
| } |
| // XXX: Really the constraint should be that unusedBitsAtEnd must be less |
| // than 7. But, we suspect there are no real-world values in OCSP responses |
| // or certificates with non-zero unused bits. It seems like NSS assumes this |
| // in various places, so we enforce it too in order to simplify this code. If |
| // we find compatibility issues, we'll know we're wrong and we'll have to |
| // figure out how to shift the bits around. |
| if (unusedBitsAtEnd != 0) { |
| return Result::ERROR_BAD_DER; |
| } |
| return valueWithUnusedBits.SkipToEnd(value); |
| } |
| |
| static inline Result |
| ReadDigit(Reader& input, /*out*/ unsigned int& value) |
| { |
| uint8_t b; |
| if (input.Read(b) != Success) { |
| return Result::ERROR_INVALID_DER_TIME; |
| } |
| if (b < '0' || b > '9') { |
| return Result::ERROR_INVALID_DER_TIME; |
| } |
| value = static_cast<unsigned int>(b - static_cast<uint8_t>('0')); |
| return Success; |
| } |
| |
| static inline Result |
| ReadTwoDigits(Reader& input, unsigned int minValue, unsigned int maxValue, |
| /*out*/ unsigned int& value) |
| { |
| unsigned int hi; |
| Result rv = ReadDigit(input, hi); |
| if (rv != Success) { |
| return rv; |
| } |
| unsigned int lo; |
| rv = ReadDigit(input, lo); |
| if (rv != Success) { |
| return rv; |
| } |
| value = (hi * 10) + lo; |
| if (value < minValue || value > maxValue) { |
| return Result::ERROR_INVALID_DER_TIME; |
| } |
| return Success; |
| } |
| |
| namespace internal { |
| |
| // We parse GeneralizedTime and UTCTime according to RFC 5280 and we do not |
| // accept all time formats allowed in the ASN.1 spec. That is, |
| // GeneralizedTime must always be in the format YYYYMMDDHHMMSSZ and UTCTime |
| // must always be in the format YYMMDDHHMMSSZ. Timezone formats of the form |
| // +HH:MM or -HH:MM or NOT accepted. |
| Result |
| TimeChoice(Reader& tagged, uint8_t expectedTag, /*out*/ Time& time) |
| { |
| unsigned int days; |
| |
| Reader input; |
| Result rv = ExpectTagAndGetValue(tagged, expectedTag, input); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| unsigned int yearHi; |
| unsigned int yearLo; |
| if (expectedTag == GENERALIZED_TIME) { |
| rv = ReadTwoDigits(input, 0, 99, yearHi); |
| if (rv != Success) { |
| return rv; |
| } |
| rv = ReadTwoDigits(input, 0, 99, yearLo); |
| if (rv != Success) { |
| return rv; |
| } |
| } else if (expectedTag == UTCTime) { |
| rv = ReadTwoDigits(input, 0, 99, yearLo); |
| if (rv != Success) { |
| return rv; |
| } |
| yearHi = yearLo >= 50u ? 19u : 20u; |
| } else { |
| return NotReached("invalid tag given to TimeChoice", |
| Result::ERROR_INVALID_DER_TIME); |
| } |
| unsigned int year = (yearHi * 100u) + yearLo; |
| if (year < 1970u) { |
| // We don't support dates before January 1, 1970 because that is the epoch. |
| return Result::ERROR_INVALID_DER_TIME; |
| } |
| days = DaysBeforeYear(year); |
| |
| unsigned int month; |
| rv = ReadTwoDigits(input, 1u, 12u, month); |
| if (rv != Success) { |
| return rv; |
| } |
| unsigned int daysInMonth; |
| static const unsigned int jan = 31u; |
| const unsigned int feb = ((year % 4u == 0u) && |
| ((year % 100u != 0u) || (year % 400u == 0u))) |
| ? 29u |
| : 28u; |
| static const unsigned int mar = 31u; |
| static const unsigned int apr = 30u; |
| static const unsigned int may = 31u; |
| static const unsigned int jun = 30u; |
| static const unsigned int jul = 31u; |
| static const unsigned int aug = 31u; |
| static const unsigned int sep = 30u; |
| static const unsigned int oct = 31u; |
| static const unsigned int nov = 30u; |
| static const unsigned int dec = 31u; |
| switch (month) { |
| case 1: daysInMonth = jan; break; |
| case 2: daysInMonth = feb; days += jan; break; |
| case 3: daysInMonth = mar; days += jan + feb; break; |
| case 4: daysInMonth = apr; days += jan + feb + mar; break; |
| case 5: daysInMonth = may; days += jan + feb + mar + apr; break; |
| case 6: daysInMonth = jun; days += jan + feb + mar + apr + may; break; |
| case 7: daysInMonth = jul; days += jan + feb + mar + apr + may + jun; |
| break; |
| case 8: daysInMonth = aug; days += jan + feb + mar + apr + may + jun + |
| jul; |
| break; |
| case 9: daysInMonth = sep; days += jan + feb + mar + apr + may + jun + |
| jul + aug; |
| break; |
| case 10: daysInMonth = oct; days += jan + feb + mar + apr + may + jun + |
| jul + aug + sep; |
| break; |
| case 11: daysInMonth = nov; days += jan + feb + mar + apr + may + jun + |
| jul + aug + sep + oct; |
| break; |
| case 12: daysInMonth = dec; days += jan + feb + mar + apr + may + jun + |
| jul + aug + sep + oct + nov; |
| break; |
| default: |
| return NotReached("month already bounds-checked by ReadTwoDigits", |
| Result::FATAL_ERROR_INVALID_STATE); |
| } |
| |
| unsigned int dayOfMonth; |
| rv = ReadTwoDigits(input, 1u, daysInMonth, dayOfMonth); |
| if (rv != Success) { |
| return rv; |
| } |
| days += dayOfMonth - 1; |
| |
| unsigned int hours; |
| rv = ReadTwoDigits(input, 0u, 23u, hours); |
| if (rv != Success) { |
| return rv; |
| } |
| unsigned int minutes; |
| rv = ReadTwoDigits(input, 0u, 59u, minutes); |
| if (rv != Success) { |
| return rv; |
| } |
| unsigned int seconds; |
| rv = ReadTwoDigits(input, 0u, 59u, seconds); |
| if (rv != Success) { |
| return rv; |
| } |
| |
| uint8_t b; |
| if (input.Read(b) != Success) { |
| return Result::ERROR_INVALID_DER_TIME; |
| } |
| if (b != 'Z') { |
| return Result::ERROR_INVALID_DER_TIME; |
| } |
| if (End(input) != Success) { |
| return Result::ERROR_INVALID_DER_TIME; |
| } |
| |
| uint64_t totalSeconds = (static_cast<uint64_t>(days) * 24u * 60u * 60u) + |
| (static_cast<uint64_t>(hours) * 60u * 60u) + |
| (static_cast<uint64_t>(minutes) * 60u) + |
| seconds; |
| |
| time = TimeFromElapsedSecondsAD(totalSeconds); |
| return Success; |
| } |
| |
| Result |
| IntegralBytes(Reader& input, uint8_t tag, |
| IntegralValueRestriction valueRestriction, |
| /*out*/ Input& value, |
| /*optional out*/ Input::size_type* significantBytes) |
| { |
| Result rv = ExpectTagAndGetValue(input, tag, value); |
| if (rv != Success) { |
| return rv; |
| } |
| Reader reader(value); |
| |
| // There must be at least one byte in the value. (Zero is encoded with a |
| // single 0x00 value byte.) |
| uint8_t firstByte; |
| rv = reader.Read(firstByte); |
| if (rv != Success) { |
| if (rv == Result::ERROR_BAD_DER) { |
| return Result::ERROR_INVALID_INTEGER_ENCODING; |
| } |
| |
| return rv; |
| } |
| |
| // If there is a byte after an initial 0x00/0xFF, then the initial byte |
| // indicates a positive/negative integer value with its high bit set/unset. |
| bool prefixed = !reader.AtEnd() && (firstByte == 0 || firstByte == 0xff); |
| |
| if (prefixed) { |
| uint8_t nextByte; |
| if (reader.Read(nextByte) != Success) { |
| return NotReached("Read of one byte failed but not at end.", |
| Result::FATAL_ERROR_LIBRARY_FAILURE); |
| } |
| if ((firstByte & 0x80) == (nextByte & 0x80)) { |
| return Result::ERROR_INVALID_INTEGER_ENCODING; |
| } |
| } |
| |
| switch (valueRestriction) { |
| case IntegralValueRestriction::MustBe0To127: |
| if (value.GetLength() != 1 || (firstByte & 0x80) != 0) { |
| return Result::ERROR_INVALID_INTEGER_ENCODING; |
| } |
| break; |
| |
| case IntegralValueRestriction::MustBePositive: |
| if ((value.GetLength() == 1 && firstByte == 0) || |
| (firstByte & 0x80) != 0) { |
| return Result::ERROR_INVALID_INTEGER_ENCODING; |
| } |
| break; |
| |
| case IntegralValueRestriction::NoRestriction: |
| break; |
| } |
| |
| if (significantBytes) { |
| *significantBytes = value.GetLength(); |
| if (prefixed) { |
| assert(*significantBytes > 1); |
| --*significantBytes; |
| } |
| |
| assert(*significantBytes > 0); |
| } |
| |
| return Success; |
| } |
| |
| // This parser will only parse values between 0..127. If this range is |
| // increased then callers will need to be changed. |
| Result |
| IntegralValue(Reader& input, uint8_t tag, /*out*/ uint8_t& value) |
| { |
| // Conveniently, all the Integers that we actually have to be able to parse |
| // are positive and very small. Consequently, this parser is *much* simpler |
| // than a general Integer parser would need to be. |
| Input valueBytes; |
| Result rv = IntegralBytes(input, tag, IntegralValueRestriction::MustBe0To127, |
| valueBytes, nullptr); |
| if (rv != Success) { |
| return rv; |
| } |
| Reader valueReader(valueBytes); |
| rv = valueReader.Read(value); |
| if (rv != Success) { |
| return NotReached("IntegralBytes already validated the value.", rv); |
| } |
| rv = End(valueReader); |
| assert(rv == Success); // guaranteed by IntegralBytes's range checks. |
| return rv; |
| } |
| |
| } // namespace internal |
| |
| Result |
| OptionalVersion(Reader& input, /*out*/ Version& version) |
| { |
| static const uint8_t TAG = CONTEXT_SPECIFIC | CONSTRUCTED | 0; |
| if (!input.Peek(TAG)) { |
| version = Version::v1; |
| return Success; |
| } |
| return Nested(input, TAG, [&version](Reader& value) -> Result { |
| uint8_t integerValue; |
| Result rv = Integer(value, integerValue); |
| if (rv != Success) { |
| return rv; |
| } |
| // XXX(bug 1031093): We shouldn't accept an explicit encoding of v1, |
| // but we do here for compatibility reasons. |
| switch (integerValue) { |
| case static_cast<uint8_t>(Version::v3): version = Version::v3; break; |
| case static_cast<uint8_t>(Version::v2): version = Version::v2; break; |
| case static_cast<uint8_t>(Version::v1): version = Version::v1; break; |
| case static_cast<uint8_t>(Version::v4): version = Version::v4; break; |
| default: |
| return Result::ERROR_BAD_DER; |
| } |
| return Success; |
| }); |
| } |
| |
| } } } // namespace mozilla::pkix::der |