| # 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 |