Skip to content

Commit

Permalink
Update to Developer Beta 3 build. (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lukasa authored Jul 31, 2020
1 parent 6445f9f commit d82f7d8
Show file tree
Hide file tree
Showing 13 changed files with 1,120 additions and 4,910 deletions.
43 changes: 33 additions & 10 deletions Sources/Crypto/ASN1/ASN1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ import Foundation
// generally: that is, we don't hard-code special knowledge of these formats as part of the parsing process. Instead we have written a
// parser that can divide the world of ASN.1 into parseable chunks, and then we try to decode specific formats from those chunks. This
// allows us to extend things in the future without too much pain.

internal enum ASN1 { }

// MARK: - Parser Node
Expand Down Expand Up @@ -228,14 +227,19 @@ extension ASN1 {
}

let identifier = try ASN1Identifier(rawIdentifier: rawIdentifier)
guard let length = try data.readASN1Length() else {
guard let wideLength = try data.readASN1Length() else {
throw CryptoKitASN1Error.truncatedASN1Field
}

var subData = data.prefix(Int(length))
data = data.dropFirst(Int(length))
// UInt is sometimes too large for us!
guard let length = Int(exactly: wideLength) else {
throw CryptoKitASN1Error.invalidASN1Object
}

var subData = data.prefix(length)
data = data.dropFirst(length)

guard subData.count == Int(length) else {
guard subData.count == length else {
throw CryptoKitASN1Error.truncatedASN1Field
}

Expand Down Expand Up @@ -486,17 +490,36 @@ extension ArraySlice where Element == UInt8 {
// We need to read the length bytes
let lengthBytes = self.prefix(fieldLength)
self = self.dropFirst(fieldLength)
return try UInt(bigEndianBytes: lengthBytes)
let length = try UInt(bigEndianBytes: lengthBytes)

// DER requires that we enforce that the length field was encoded in the minimum number of octets necessary.
let requiredBits = UInt.bitWidth - length.leadingZeroBitCount
switch requiredBits {
case 0...7:
// For 0 to 7 bits, the long form is unnacceptable and we require the short.
throw CryptoKitASN1Error.unsupportedFieldLength
case 8...:
// For 8 or more bits, fieldLength should be the minimum required.
let requiredBytes = (requiredBits + 7) / 8
if fieldLength > requiredBytes {
throw CryptoKitASN1Error.unsupportedFieldLength
}
default:
// This is not reachable, but we'll error anyway.
throw CryptoKitASN1Error.unsupportedFieldLength
}

return length
case let val:
// Short form, the length is only one 7-bit integer.
return UInt(val)
}
}
}

extension UInt {
extension FixedWidthInteger {
internal init<Bytes: Collection>(bigEndianBytes bytes: Bytes) throws where Bytes.Element == UInt8 {
guard bytes.count <= MemoryLayout<UInt>.size else {
guard bytes.count <= (Self.bitWidth / 8) else {
throw CryptoKitASN1Error.invalidASN1Object
}

Expand All @@ -505,7 +528,7 @@ extension UInt {

var index = bytes.startIndex
for shift in shiftSizes {
self |= UInt(bytes[index]) << shift
self |= Self(truncatingIfNeeded: bytes[index]) << shift
bytes.formIndex(after: &index)
}
}
Expand Down Expand Up @@ -553,7 +576,7 @@ extension Int {
}
}

extension UInt {
extension FixedWidthInteger {
// Bytes needed to store a given integer.
internal var neededBytes: Int {
let neededBits = self.bitWidth - self.leadingZeroBitCount
Expand Down
230 changes: 211 additions & 19 deletions Sources/Crypto/ASN1/Basic ASN1 Types/ASN1Integer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,31 @@
#else
import Foundation

// Going forward this should probably be a real separate type, but it's somewhat convenient for now to just extend `Int`.
extension Int: ASN1Parseable, ASN1Serializable {
/// A protocol that represents any internal object that can present itself as a INTEGER, or be parsed from
/// a INTEGER.
///
/// This is not a very good solution for a fully-fledged ASN.1 library: we'd rather have a better numerics
/// protocol that could both initialize from and serialize to either bytes or words. However, no such
/// protocol exists today.
protocol ASN1IntegerRepresentable: ASN1Parseable, ASN1Serializable {
associatedtype IntegerBytes: RandomAccessCollection where IntegerBytes.Element == UInt8

/// Whether this type can represent signed integers. If this is set to false, the serializer and
/// parser will automatically handle padding with leading zero bytes as needed.
static var isSigned: Bool { get }

init(asn1IntegerBytes: ArraySlice<UInt8>) throws

func withBigEndianIntegerBytes<ReturnType>(_ body: (IntegerBytes) throws -> ReturnType) rethrows -> ReturnType
}

extension ASN1IntegerRepresentable {
internal init(asn1Encoded node: ASN1.ASN1Node) throws {
guard node.identifier == .integer else {
throw CryptoKitASN1Error.unexpectedFieldType
}

guard case .primitive(let dataBytes) = node.content else {
guard case .primitive(var dataBytes) = node.content else {
preconditionFailure("ASN.1 parser generated primitive node with constructed content")
}

Expand All @@ -39,37 +56,212 @@ extension Int: ASN1Parseable, ASN1Serializable {
//
// NOTE – These rules ensure that an integer value is always encoded in the smallest possible number of octets.
if let first = dataBytes.first, let second = dataBytes.dropFirst().first {
if ((first & 0xFF == 0xFF) && (second & 0x80 == 0x80)) ||
((first & 0xFF == 0x00) && (second & 0x80 == 0x00)) {
if (first == 0xFF) && second.topBitSet ||
(first == 0x00) && !second.topBitSet {
throw CryptoKitASN1Error.invalidASN1IntegerEncoding
}
}
self = try Int(bigEndianBytes: dataBytes)

// If the type we're trying to decode is unsigned, and the top byte is zero, we should strip it.
// If the top bit is set, however, this is an invalid conversion: the number needs to be positive!
if !Self.isSigned, let first = dataBytes.first {
if first == 0x00 {
dataBytes = dataBytes.dropFirst()
} else if first & 0x80 == 0x80 {
throw CryptoKitASN1Error.invalidASN1IntegerEncoding
}
}

self = try Self(asn1IntegerBytes: dataBytes)
}

internal func serialize(into coder: inout ASN1.Serializer) throws {
coder.appendPrimitiveNode(identifier: .integer) { bytes in
// We need to use the minimal number of bytes here.
let neededBytes = UInt(self).neededBytes
self.withBigEndianIntegerBytes { integerBytes in
// If the number of bytes is 0, we're encoding a zero. That actually _does_ require one byte.
if integerBytes.count == 0 {
bytes.append(0)
return
}

// If self is unsigned and the first byte has the top bit set, we need to prepend a 0 byte.
if !Self.isSigned, let topByte = integerBytes.first, topByte.topBitSet {
bytes.append(0)
bytes.append(contentsOf: integerBytes)
} else {
// Either self is signed, or the top bit isn't set. Either way, trim to make sure the representation is minimal.
bytes.append(contentsOf: integerBytes.trimLeadingExcessBytes())
}
}
}
}
}

// MARK: - Auto-conformance for FixedWidthInteger with fixed width magnitude.
extension ASN1IntegerRepresentable where Self: FixedWidthInteger {
init(asn1IntegerBytes bytes: ArraySlice<UInt8>) throws {
// Defer to the FixedWidthInteger constructor.
// There's a wrinkle here: if this is a signed integer, and the top bit of the data bytes was set,
// then we need to 1-extend the bytes. This is because ASN.1 tries to delete redundant bytes that
// are all 1.
self = try Self(bigEndianBytes: bytes)

if Self.isSigned, let first = bytes.first, first.topBitSet {
for shift in stride(from: self.bitWidth - self.leadingZeroBitCount, to: self.bitWidth, by: 8) {
self |= 0xFF << shift
}
}
}

func withBigEndianIntegerBytes<ReturnType>(_ body: (IntegerBytesCollection<Self>) throws -> ReturnType) rethrows -> ReturnType {
return try body(IntegerBytesCollection(self))
}
}

struct IntegerBytesCollection<Integer: FixedWidthInteger> {
private var integer: Integer

init(_ integer: Integer) {
self.integer = integer
}
}

extension IntegerBytesCollection: RandomAccessCollection {
struct Index {
fileprivate var byteNumber: Int

fileprivate init(byteNumber: Int) {
self.byteNumber = byteNumber
}

fileprivate var shift: Integer {
// As byte number 0 is the end index, the byte number is one byte too large for the shift.
return Integer((self.byteNumber - 1) * 8)
}
}

var startIndex: Index {
return Index(byteNumber: Int(self.integer.neededBytes))
}

var endIndex: Index {
return Index(byteNumber: 0)
}

var count: Int {
return Int(self.integer.neededBytes)
}

subscript(index: Index) -> UInt8 {
// We perform the bitwise operations in magnitude space.
let shifted = Integer.Magnitude(truncatingIfNeeded: self.integer) >> index.shift
let masked = shifted & 0xFF
return UInt8(masked)
}
}

extension IntegerBytesCollection.Index: Equatable { }

extension IntegerBytesCollection.Index: Comparable {
// Comparable here is backwards to the original ordering.
static func <(lhs: Self, rhs: Self) -> Bool {
return lhs.byteNumber > rhs.byteNumber
}

static func >(lhs: Self, rhs: Self) -> Bool {
return lhs.byteNumber < rhs.byteNumber
}

static func <=(lhs: Self, rhs: Self) -> Bool {
return lhs.byteNumber >= rhs.byteNumber
}

static func >=(lhs: Self, rhs: Self) -> Bool {
return lhs.byteNumber <= rhs.byteNumber
}
}

extension IntegerBytesCollection.Index: Strideable {
func advanced(by n: Int) -> IntegerBytesCollection<Integer>.Index {
return IntegerBytesCollection.Index(byteNumber: self.byteNumber - n)
}

func distance(to other: IntegerBytesCollection<Integer>.Index) -> Int {
// Remember that early indices have high byte numbers and later indices have low ones.
return self.byteNumber - other.byteNumber
}
}

// If needed bytes is 0, we're encoding a zero. That actually _does_ require one byte.
if neededBytes == 0 {
bytes.append(0)
return
extension Int8: ASN1IntegerRepresentable { }

extension UInt8: ASN1IntegerRepresentable { }

extension Int16: ASN1IntegerRepresentable { }

extension UInt16: ASN1IntegerRepresentable { }

extension Int32: ASN1IntegerRepresentable { }

extension UInt32: ASN1IntegerRepresentable { }

extension Int64: ASN1IntegerRepresentable { }

extension UInt64: ASN1IntegerRepresentable { }

extension Int: ASN1IntegerRepresentable { }

extension UInt: ASN1IntegerRepresentable { }

extension RandomAccessCollection where Element == UInt8 {
fileprivate func trimLeadingExcessBytes() -> SubSequence {
var slice = self[...]
guard let first = slice.first else {
// Easy case, empty.
return slice
}

let wholeByte: UInt8

switch first {
case 0:
wholeByte = 0
case 0xFF:
wholeByte = 0xFF
default:
// We're already fine, this is maximally compact. We need the whole thing.
return slice
}

// We never trim this to less than one byte, as that's always the smallest representation.
while slice.count > 1 {
// If the first byte is equal to our original first byte, and the top bit
// of the next byte is also equal to that, then we need to drop the byte and
// go again.
if slice.first != wholeByte {
break
}

for byteNumber in (0..<neededBytes).reversed() {
let shift = byteNumber * 8
bytes.append(UInt8((self >> shift) & 0xFF))
guard let second = slice.dropFirst().first else {
preconditionFailure("Loop condition violated: must be at least two bytes left")
}

if second & 0x80 != wholeByte & 0x80 {
// Different top bit, we need the leading byte.
break
}

// Both the first byte and the top bit of the next are all zero or all 1, drop the leading
// byte.
slice = slice.dropFirst()
}

return slice
}
}

extension Int {
fileprivate init<Bytes: Collection>(bigEndianBytes bytes: Bytes) throws where Bytes.Element == UInt8 {
let bitPattern = try UInt(bigEndianBytes: bytes)
self = Int(bitPattern: bitPattern)
extension UInt8 {
fileprivate var topBitSet: Bool {
return (self & 0x80) != 0
}
}

Expand Down
37 changes: 37 additions & 0 deletions Sources/Crypto/ASN1/Basic ASN1 Types/ArraySliceBigint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.md for the list of SwiftCrypto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API
@_exported import CryptoKit
#else

// For temporary purposes we pretend that ArraySlice is our "bigint" type. We don't really need anything else.
extension ArraySlice: ASN1Serializable where Element == UInt8 { }

extension ArraySlice: ASN1Parseable where Element == UInt8 { }

extension ArraySlice: ASN1IntegerRepresentable where Element == UInt8 {
// We only use unsigned "bigint"s
static var isSigned: Bool {
return false
}

init(asn1IntegerBytes: ArraySlice<UInt8>) throws {
self = asn1IntegerBytes
}

func withBigEndianIntegerBytes<ReturnType>(_ body: (ArraySlice<UInt8>) throws -> ReturnType) rethrows -> ReturnType {
return try body(self)
}
}
#endif
Loading

0 comments on commit d82f7d8

Please sign in to comment.