# Copyright 2016-2020 The OpenSSL Project Authors. All Rights Reserved.
#
# Licensed under the Apache License 2.0 (the "License"). You may not use
# this file except in compliance with the License. You can obtain a copy
# in the file LICENSE in the source distribution or at
# https://www.openssl.org/source/license.html
# Author note: this is originally RL::ASN1::OID,
# repurposed by the author for OpenSSL use.
package OpenSSL::OID;
use 5.10.0;
use strict;
use warnings;
use Carp;
use Exporter;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
@ISA = qw(Exporter);
@EXPORT = qw(parse_oid encode_oid register_oid
registered_oid_arcs registered_oid_leaves);
@EXPORT_OK = qw(encode_oid_nums);
# Unfortunately, the pairwise List::Util functionality came with perl
# v5.19.3, and I want to target absolute compatibility with perl 5.10
# and up. That means I have to implement quick pairwise functions here.
#use List::Util;
sub _pairs (@);
sub _pairmap (&@);
=head1 NAME
OpenSSL::OID - an OBJECT IDENTIFIER parser / encoder
=head1 VERSION
Version 0.1
=cut
our $VERSION = '0.1';
=head1 SYNOPSIS
use OpenSSL::OID;
# This gives the array ( 1 2 840 113549 1 1 )
my @nums = parse_oid('{ pkcs-1 1 }');
# This gives the array of DER encoded bytes for the OID, i.e.
# ( 42, 134, 72, 134, 247, 13, 1, 1 )
my @bytes = encode_oid('{ pkcs-1 1 }');
# This registers a name with an OID. It's saved internally and
# serves as repository of names for further parsing, such as 'pkcs-1'
# in the strings used above.
register_object('pkcs-1', '{ pkcs 1 }');
use OpenSSL::OID qw(:DEFAULT encode_oid_nums);
# This does the same as encode_oid(), but takes the output of
# parse_oid() as input.
my @bytes = encode_oid_nums(@nums);
=head1 EXPORT
The functions parse_oid and encode_oid are exported by default.
The function encode_oid_nums() can be exported explicitly.
=cut
######## REGEXPS
# ASN.1 object identifiers come in two forms: 1) the bracketed form
#(referred to as ObjectIdentifierValue in X.690), 2) the dotted form
#(referred to as XMLObjIdentifierValue in X.690)
#
# examples of 1 (these are all the OID for rsaEncrypted):
#
# { iso (1) 2 840 11349 1 1 }
# { pkcs 1 1 }
# { pkcs1 1 }
#
# examples of 2:
#
# 1.2.840.113549.1.1
# pkcs.1.1
# pkcs1.1
#
my $identifier_re = qr/[a-z](?:[-_A-Za-z0-9]*[A-Za-z0-9])?/;
# The only difference between $objcomponent_re and $xmlobjcomponent_re is
# the separator in the top branch. Each component is always parsed in two
# groups, so we get a pair of values regardless. That's the reason for the
# empty parentheses.
# Because perl doesn't try to do an exhaustive try of every branch it rather
# stops on the first that matches, we need to have them in order of longest
# to shortest where there may be ambiguity.
my $objcomponent_re = qr/(?|
(${identifier_re}) \s* \((\d+)\)
|
(${identifier_re}) ()
|
()(\d+)
)/x;
my $xmlobjcomponent_re = qr/(?|
(${identifier_re}) \. \((\d+)\)
|
(${identifier_re}) ()
|
() (\d+)
)/x;
my $obj_re =
qr/(?: \{ \s* (?: ${objcomponent_re} \s+ )* ${objcomponent_re} \s* \} )/x;
my $xmlobj_re =
qr/(?: (?: ${xmlobjcomponent_re} \. )* ${xmlobjcomponent_re} )/x;
######## NAME TO OID REPOSITORY
# Recorded OIDs, to support things like '{ pkcs1 1 }'
# Do note that we don't currently support relative OIDs
#
# The key is the identifier.
#
# The value is a hash, composed of:
# type => 'arc' | 'leaf'
# nums => [ LIST ]
# Note that the |type| always starts as a 'leaf', and may change to an 'arc'
# on the fly, as new OIDs are parsed.
my %name2oid = ();
########
=head1 SUBROUTINES/METHODS
=over 4
=item parse_oid()
TBA
=cut
sub parse_oid {
my $input = shift;
croak "Invalid extra arguments" if (@_);
# The components become a list of ( identifier, number ) pairs,
# where they can also be the empty string if they are not present
# in the input.
my @components;
if ($input =~ m/^\s*(${obj_re})\s*$/x) {
my $oid = $1;
@components = ( $oid =~ m/${objcomponent_re}\s*/g );
} elsif ($input =~ m/^\s*(${xmlobj_re})\s*$/) {
my $oid = $1;
@components = ( $oid =~ m/${xmlobjcomponent_re}\.?/g );
}
croak "Invalid ASN.1 object '$input'" unless @components;
die "Internal error when parsing '$input'"
unless scalar(@components) % 2 == 0;
# As we currently only support a name without number as first
# component, the easiest is to have a direct look at it and
# hack it.
my @first = _pairmap {
my ($a, $b) = @$_;
return $b if $b ne '';
return @{$name2oid{$a}->{nums}} if $a ne '' && defined $name2oid{$a};
croak "Undefined identifier $a" if $a ne '';
croak "Empty OID element (how's that possible?)";
} ( @components[0..1] );
my @numbers =
(
@first,
_pairmap {
my ($a, $b) = @$_;
return $b if $b ne '';
croak "Unsupported relative OID $a" if $a ne '';
croak "Empty OID element (how's that possible?)";
} @components[2..$#components]
);
# If the first component has an identifier and there are other
# components following it, we change the type of that identifier
# to 'arc'.
if (scalar @components > 2
&& $components[0] ne ''
&& defined $name2oid{$components[0]}) {
$name2oid{$components[0]}->{type} = 'arc';
}
return @numbers;
}
=item encode_oid()
=cut
# Forward declaration
sub encode_oid_nums;
sub encode_oid {
return encode_oid_nums parse_oid @_;
}
=item register_oid()
=cut
sub register_oid {
my $name = shift;
my @nums = parse_oid @_;
if (defined $name2oid{$name}) {
my $str1 = join(',', @nums);
my $str2 = join(',', @{$name2oid{$name}->{nums}});
croak "Invalid redefinition of $name with different value"
unless $str1 eq $str2;
} else {
$name2oid{$name} = { type => 'leaf', nums => [ @nums ] };
}
}
=item registered_oid_arcs()
=item registered_oid_leaves()
=cut
sub _registered_oids {
my $type = shift;
return grep { $name2oid{$_}->{type} eq $type } keys %name2oid;
}
sub registered_oid_arcs {
return _registered_oids( 'arc' );
}
sub registered_oid_leaves {
return _registered_oids( 'leaf' );
}
=item encode_oid_nums()
=cut
# Internal helper. It takes a numeric OID component and generates the
# DER encoding for it.
sub _gen_oid_bytes {
my $num = shift;
my $cnt = 0;
return ( $num ) if $num < 128;
return ( ( map { $_ | 0x80 } _gen_oid_bytes($num >> 7) ), $num & 0x7f );
}
sub encode_oid_nums {
my @numbers = @_;
croak 'Invalid OID values: ( ', join(', ', @numbers), ' )'
if (scalar @numbers < 2
|| $numbers[0] < 0 || $numbers[0] > 2
|| $numbers[1] < 0 || $numbers[1] > 39);
my $first = shift(@numbers) * 40 + shift(@numbers);
@numbers = ( $first, map { _gen_oid_bytes($_) } @numbers );
return @numbers;
}
=back
=head1 AUTHOR
Richard levitte, C<< <richard at levitte.org> >>
=cut
######## Helpers
sub _pairs (@) {
croak "Odd number of arguments" if @_ & 1;
my @pairlist = ();
while (@_) {
my $x = [ shift, shift ];
push @pairlist, $x;
}
return @pairlist;
}
sub _pairmap (&@) {
my $block = shift;
map { $block->($_) } _pairs @_;
}
1; # End of OpenSSL::OID